commit 29a1ec213f7e206784b7c5b66a3048e268a8d590 Author: ficedula Date: Wed Jul 20 19:46:44 2022 +0100 Go diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..b03bd47 --- /dev/null +++ b/.hgignore @@ -0,0 +1,4 @@ +syntax: glob +.vs/* +*/obj/* +*/bin/* diff --git a/F7.sln b/F7.sln new file mode 100644 index 0000000..03d84ac --- /dev/null +++ b/F7.sln @@ -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 diff --git a/F7/Content/Content.mgcb b/F7/Content/Content.mgcb new file mode 100644 index 0000000..ad62662 --- /dev/null +++ b/F7/Content/Content.mgcb @@ -0,0 +1,15 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:Windows +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + diff --git a/F7/F7.csproj b/F7/F7.csproj new file mode 100644 index 0000000..53995fc --- /dev/null +++ b/F7/F7.csproj @@ -0,0 +1,26 @@ + + + WinExe + net6.0-windows + false + false + true + + + app.manifest + Icon.ico + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/F7/FGame.cs b/F7/FGame.cs new file mode 100644 index 0000000..be7d5a0 --- /dev/null +++ b/F7/FGame.cs @@ -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> _data = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + public FGame(string data, string bdata) { + _data["field"] = new List { + 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(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}"); + } + } +} diff --git a/F7/Field/Background.cs b/F7/Field/Background.cs new file mode 100644 index 0000000..776e007 --- /dev/null +++ b/F7/Field/Background.cs @@ -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 Sprites; + public List Data; + public BlendType Blend; + public int Parameter; + public int Mask; + public int OffsetX, OffsetY; + } + + private List _layers = new(); + private Ficedula.FF7.Field.Background _bg; + private GraphicsDevice _graphics; + private BasicEffect _effect; + private Dictionary _parameters = new(); + + private void Draw(IEnumerable sprites, List 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 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); + } + } + } + } +} diff --git a/F7/Field/Entity.cs b/F7/Field/Entity.cs new file mode 100644 index 0000000..9649a85 --- /dev/null +++ b/F7/Field/Entity.cs @@ -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--; + } + } + + } +} diff --git a/F7/Field/FieldDebug.cs b/F7/Field/FieldDebug.cs new file mode 100644 index 0000000..70a9f69 --- /dev/null +++ b/F7/Field/FieldDebug.cs @@ -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 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); + } + } + } +} diff --git a/F7/Field/FieldModel.cs b/F7/Field/FieldModel.cs new file mode 100644 index 0000000..9031512 --- /dev/null +++ b/F7/Field/FieldModel.cs @@ -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 _nodes = new(); + private BasicEffect _texEffect, _colEffect; + private VertexBuffer _vertexBuffer; + private IndexBuffer _indexBuffer; + private Ficedula.FF7.Field.HRCModel _hrcModel; + private GraphicsDevice _graphics; + private List _animations = new(); + + //TODO dedupe textures + public FieldModel(GraphicsDevice graphics, FGame g, string hrc, IEnumerable animations) { + _graphics = graphics; + _hrcModel = new Ficedula.FF7.Field.HRCModel(s => g.Open("field", s), hrc); + + List verts = new(); + List 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 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; + } + + } +} diff --git a/F7/Field/FieldScreen.cs b/F7/Field/FieldScreen.cs new file mode 100644 index 0000000..4bd4b11 --- /dev/null +++ b/F7/Field/FieldScreen.cs @@ -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 Entities { get; } + public FieldModel Player { get; set; } + public List 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; + } + } + } + } +} diff --git a/F7/Field/VM.cs b/F7/Field/VM.cs new file mode 100644 index 0000000..f8f582b --- /dev/null +++ b/F7/Field/VM.cs @@ -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; + } + } +} diff --git a/F7/Game1.cs b/F7/Game1.cs new file mode 100644 index 0000000..35d2b17 --- /dev/null +++ b/F7/Game1.cs @@ -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 _keyMap = new Dictionary { + [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(); + } + } +} \ No newline at end of file diff --git a/F7/Icon.ico b/F7/Icon.ico new file mode 100644 index 0000000..7d9dec1 Binary files /dev/null and b/F7/Icon.ico differ diff --git a/F7/Input.cs b/F7/Input.cs new file mode 100644 index 0000000..ce20512 --- /dev/null +++ b/F7/Input.cs @@ -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 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()) + DownFor[k] = 0; + } + } +} diff --git a/F7/Program.cs b/F7/Program.cs new file mode 100644 index 0000000..ee92393 --- /dev/null +++ b/F7/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace F7 +{ + public static class Program + { + [STAThread] + static void Main() + { + using (var game = new Game1()) + game.Run(); + } + } +} diff --git a/F7/SaveData.cs b/F7/SaveData.cs new file mode 100644 index 0000000..e1d7746 --- /dev/null +++ b/F7/SaveData.cs @@ -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 WeaponMateria { get; set; } + public List ArmourMateria { get; set; } + + [XmlIgnore] + public float LevelProgress => 0.7f; //TODO!!! + + [XmlIgnore] + public bool IsBackRow => Flags.HasFlag(CharFlags.BackRow); + } + + public class SaveData { + + private IEnumerable 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 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); + } + } + } +} diff --git a/F7/Screen.cs b/F7/Screen.cs new file mode 100644 index 0000000..63befb7 --- /dev/null +++ b/F7/Screen.cs @@ -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) { } + } +} diff --git a/F7/TestScreen.cs b/F7/TestScreen.cs new file mode 100644 index 0000000..85a5e44 --- /dev/null +++ b/F7/TestScreen.cs @@ -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(); + } + } +} diff --git a/F7/Util.cs b/F7/Util.cs new file mode 100644 index 0000000..ab5af5d --- /dev/null +++ b/F7/Util.cs @@ -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(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; } } + } + +} diff --git a/F7/VMM.cs b/F7/VMM.cs new file mode 100644 index 0000000..86e43f4 --- /dev/null +++ b/F7/VMM.cs @@ -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}"); + } + } + } +} diff --git a/F7/Viewer.cs b/F7/Viewer.cs new file mode 100644 index 0000000..51e3108 --- /dev/null +++ b/F7/Viewer.cs @@ -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 + ); + } +} diff --git a/F7/app.manifest b/F7/app.manifest new file mode 100644 index 0000000..863d0bd --- /dev/null +++ b/F7/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/F7Cmd/F7Cmd.csproj b/F7Cmd/F7Cmd.csproj new file mode 100644 index 0000000..da5ad60 --- /dev/null +++ b/F7Cmd/F7Cmd.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/F7Cmd/Program.cs b/F7Cmd/Program.cs new file mode 100644 index 0000000..0772851 --- /dev/null +++ b/F7Cmd/Program.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/F7Cmd/Properties/launchSettings.json b/F7Cmd/Properties/launchSettings.json new file mode 100644 index 0000000..d9b6732 --- /dev/null +++ b/F7Cmd/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "F7Cmd": { + "commandName": "Project", + "commandLineArgs": "Field C:\\games\\FF7\\data\\field\\flevel.lgp mrkt2" + } + } +} \ No newline at end of file diff --git a/Ficedula.FF7/Ficedula.FF7.csproj b/Ficedula.FF7/Ficedula.FF7.csproj new file mode 100644 index 0000000..bafd05b --- /dev/null +++ b/Ficedula.FF7/Ficedula.FF7.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Ficedula.FF7/Field/Background.cs b/Ficedula.FF7/Field/Background.cs new file mode 100644 index 0000000..a72f1bf --- /dev/null +++ b/Ficedula.FF7/Field/Background.cs @@ -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 Data; + } + + public class Background { + private List _groups = new(); + + public Dictionary Pages { get; } + + public List Palettes { get; } + + public short Width { get; private set; } + public short Height { get; private set; } + public IEnumerable Layer1 { get; private set; } + public IEnumerable Layer2 { get; private set; } + public IEnumerable Layer3 { get; private set; } + public IEnumerable Layer4 { get; private set; } + public IEnumerable AllSprites => Layer1.Concat(Layer2).Concat(Layer3).Concat(Layer4); + public IEnumerable> 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 pages = new Dictionary(); + + 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() + }; + 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 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); + } + */ + } + } +} diff --git a/Ficedula.FF7/Field/DialogEvent.cs b/Ficedula.FF7/Field/DialogEvent.cs new file mode 100644 index 0000000..85b51ed --- /dev/null +++ b/Ficedula.FF7/Field/DialogEvent.cs @@ -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 Scripts { get; } + + public Entity(string name, IEnumerable scripts) { + Name = name; + Scripts = scripts.ToList(); + } + } + + public class DialogEvent { + + public string Creator { get; } + public string Name { get; } + public short Scale { get; } + public List Entities { get; } + public List 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 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 + } + } +} diff --git a/Ficedula.FF7/Field/FieldFile.cs b/Ficedula.FF7/Field/FieldFile.cs new file mode 100644 index 0000000..e3fa3b5 --- /dev/null +++ b/Ficedula.FF7/Field/FieldFile.cs @@ -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 Gateways { get; } + public List Triggers { get; } + public List 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 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 StandardEncounters { get; } + public Encounter BackAttack1 { get; set; } + public Encounter BackAttack2 { get; set; } + public Encounter SideAttack { get; set; } + public Encounter BothSidesAttack { get; set; } + + public IEnumerable 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 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(); + 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 _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 GetEncounterTables() { + _sections[6].Position = 0; + var table0 = new EncounterTable(_sections[6]); + var table1 = new EncounterTable(_sections[6]); + return new[] { table0, table1 }; + } + + public IEnumerable GetCameraMatrices() { + _sections[1].Position = 0; + List matrices = new(); + while (_sections[1].Position < _sections[1].Length) + matrices.Add(new CameraMatrix(_sections[1])); + return matrices.AsReadOnly(); + } + + } +} diff --git a/Ficedula.FF7/Field/HRCModel.cs b/Ficedula.FF7/Field/HRCModel.cs new file mode 100644 index 0000000..17dbef6 --- /dev/null +++ b/Ficedula.FF7/Field/HRCModel.cs @@ -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 Bones { get; } = new(); + public Bone Root { get; } + + public class BonePolygon { + public PFile PFile { get; } + public List Textures { get; } + + public BonePolygon(PFile pFile, List textures) { + PFile = pFile; + Textures = textures; + } + } + + public class Bone { + public List Children { get; } = new(); + public float Length { get; } + public List Polygons { get; } = new(); + public int Index { get; } + + public Bone(float length, int index) { + Length = length; + Index = index; + } + } + + + public HRCModel(Func dataProvider, string hrcFile) { + using (var hrc = dataProvider(hrcFile)) { + var lines = hrc.ReadAllLines().ToArray(); + int numBones = int.Parse(lines[2].Split(null).Last()); + Dictionary bones = new Dictionary(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 Frames { get; private set; } + + public class Frame { + public Vector3 Translation { get; } + public Vector3 Rotation { get; } + public List 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(); + } + } +} diff --git a/Ficedula.FF7/Field/ModelLoader.cs b/Ficedula.FF7/Field/ModelLoader.cs new file mode 100644 index 0000000..d1ac104 --- /dev/null +++ b/Ficedula.FF7/Field/ModelLoader.cs @@ -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 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 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(); + } + } +} diff --git a/Ficedula.FF7/LGP.cs b/Ficedula.FF7/LGP.cs new file mode 100644 index 0000000..9406dbd --- /dev/null +++ b/Ficedula.FF7/LGP.cs @@ -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 _entries; + + public IEnumerable 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 tempEntries = new List(); + 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(); + } + } +} diff --git a/Ficedula.FF7/Lzss.cs b/Ficedula.FF7/Lzss.cs new file mode 100644 index 0000000..b984b0e --- /dev/null +++ b/Ficedula.FF7/Lzss.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/Ficedula.FF7/PFile.cs b/Ficedula.FF7/PFile.cs new file mode 100644 index 0000000..fff0232 --- /dev/null +++ b/Ficedula.FF7/PFile.cs @@ -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 Verts { get; } + public List Indices { get; } + + public PFileChunk(int? texture, List verts, List indices) { + Texture = texture; + Verts = verts; + Indices = indices; + } + } + + public class PFile { + + public List 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 verts = new List(), + normals = new List(), + texcoords = new List(); + List colours = new List(); + + foreach (var group in pGroups) { + Dictionary<(int, int), int> vertMap = new(); + List gverts = new(); + List 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 + )); + } + } + } +} diff --git a/Ficedula.FF7/Streams.cs b/Ficedula.FF7/Streams.cs new file mode 100644 index 0000000..48fa96a --- /dev/null +++ b/Ficedula.FF7/Streams.cs @@ -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 ReadAllLines(this Stream s) { + using (var sr = new StreamReader(s)) { + string? line; + while ((line = sr.ReadLine()) != null) + yield return line; + } + } + } +} diff --git a/Ficedula.FF7/TexFile.cs b/Ficedula.FF7/TexFile.cs new file mode 100644 index 0000000..d5e0e17 --- /dev/null +++ b/Ficedula.FF7/TexFile.cs @@ -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 Palettes { get; } = new(); + public List 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 ApplyPalette(int which) { + var palette = Palettes[which]; + return Pixels + .Select(row => row.Select(b => palette[b]).ToArray()) + .ToList(); + } + } +} diff --git a/Ficedula.FF7/Text.cs b/Ficedula.FF7/Text.cs new file mode 100644 index 0000000..3c71787 --- /dev/null +++ b/Ficedula.FF7/Text.cs @@ -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(); + } + } +} diff --git a/Ficedula.FF7/Util.cs b/Ficedula.FF7/Util.cs new file mode 100644 index 0000000..4e3559a --- /dev/null +++ b/Ficedula.FF7/Util.cs @@ -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 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); + } + + } +} diff --git a/data/save/newgame.xml b/data/save/newgame.xml new file mode 100644 index 0000000..ffc0ca8 --- /dev/null +++ b/data/save/newgame.xml @@ -0,0 +1,83 @@ + + + + 0 + 8 + 40 + 42 + 38 + 35 + 40 + 30 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 128 + Cloud + 0 + 0 + 0 + Available Party1 + Limit1_1 + 10 + 4 + 0 + 0 + 310 + 340 + 335 + 30 + 55 + 60 + 8000 + 500 + 0 + 1 + 2 + + + 3 + 8 + 35 + 40 + 48 + 45 + 40 + 38 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 20 + Aeris + 0 + 0 + 0 + Available Party2 BackRow + Limit1_1 + 5 + 3 + 0 + 0 + 190 + 320 + 315 + 40 + 60 + 65 + 8000 + 500 + 0 + 1 + 2 + + + Train Station + \ No newline at end of file