using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace OpenVIII.World { public class Wmset : IDisposable { private const int WMSET_SECTION_COUNT = 48; private byte[] buffer; private int[] sectionPointers; /// /// wmset file is pseudo-archive containing 48 sections in which every 'chunk' has different data and meaning /// /// public Wmset(byte[] wmsetBuffer) { buffer = wmsetBuffer; sectionPointers = new int[WMSET_SECTION_COUNT]; using (var br = new BinaryReader(new MemoryStream(buffer))) { for (var i = 0; i < sectionPointers.Length; i++) sectionPointers[i] = br.ReadInt32(); } //if there's no section method either uncommented or commented out, then the section that is lacking is 4 byte NULL Section1(); //======FINISHED [Encounter setting] Section2(); //======FINISHED [World map regions] //Section3(); //=encounter - looks like it's some supply helping data or roll data? Not sure, it's way before getting encounters Section4(); //======FINISHED [Encounter pointer] Section6(); //======FINISHED [Encounter pointer (Lunar cry)] Section8(); //wm2field WIP - DEBUG data only, almost completed Section9(); //======FINISHED [Field to world map coordinates] Section11(); //?????? //Section12(); //[UNKNOWN] Scripts- maybe some additional warp zones? //Section13(); // 12b per entry- SUB_548940 Section14(); //======FINISHED [Side quest strings] Section16(); //======FINISHED [objects and vehicles] //Section18(); //????? //Section19(); //=something with regions: it's that island with many drawpoints- regions setting //Section31(); //????? - referenced by FullScreen map - prob. 12 bytes per entry, where 3rd byte is locationPointer Section32(); //======FINISHED [location names] Section33(); //=SKY GRADIENT/REGION COLOURING //Section34(); //related to 14- something with side quests //Section35(); //=draw points //Section36(); //????????? //Section37(); //Ufo, Pupu and Thrusta encounters Section38(); //======FINISHED [textures archive] Section39(); //======FINISHED [textures of roads, train tracks and bridges] Section42(); //======FINISHED [object and vehicle textures] Section41(); //======FINISHED [texture animation for water] *DO NOT MOVE UP- sec38 needs to be parsed before this Section17(); //======FINISHED [texture animation for beach] *DO NOT MOVE UP- sec38 needs to be parsed before this //AKAO BELOW //Section20(); //Section21(); //Section43(); //Section44(); //Section45(); //Section46(); //Section47(); //Section48(); //-not important in openviii implementation/ sections used for limited memory solutions/psx hardware limitations //Section7(); //helpers for roads textures- as we have it working and no issues this will be useless to reverse/investigate //Section10(); //Looks like it's a helper for model and texture on world map objects. By altering some variables //you are able to use e.g. Selphie texture on Ragnarok- 0xFF13 - arg=texIndex //Section29(); //water block for limited memory rendering- not important in today //Section40(); //can't understand this one } /// /// Every section can have inner-sections. Pointers to different textures or models /// /// /// private int[] GetInnerPointers(BinaryReader br) { var innerSections = new List(); var eof = -1; while ((eof = br.ReadInt32()) != 0) innerSections.Add(eof); return innerSections.ToArray(); } #region Section 1 - Encounter setting /// /// Section1 helps providing the correct encounter based on groundId and regionId, then provides the pointer to struct of section4 /// private const int SECTION1ENTRYCOUNT = 96; [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 4)] private struct EncounterHelper { public byte regionId; public byte groundId; public ushort encounterPointer; } private EncounterHelper[] encounterHelpEntries; private void Section1() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[1 - 1], SeekOrigin.Begin); ms.Seek(4, SeekOrigin.Current); //skip first DWORD- it's EOF of global file var encounterHelperList = new List(); while (true) { var entry = Extended.ByteArrayToStructure(br.ReadBytes(4)); if (entry.groundId == 0 && entry.regionId == 0 && entry.encounterPointer == 0) break; encounterHelperList.Add(entry); } encounterHelpEntries = encounterHelperList.ToArray(); } } public ushort GetEncounterHelperPointer(int regionId, int groundId) { var encList = encounterHelpEntries.Where(x => x.groundId == groundId && x.regionId == regionId); if (!encList.Any()) return 0xFFFF; return encList.First().encounterPointer; //always first, if there are two the same entries then it doesn't make sense- priority is over that one that is first } #endregion Section 1 - Encounter setting #region Section 2 - world map regions private byte[] regionsBuffer; private void Section2() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[2 - 1], SeekOrigin.Begin); regionsBuffer = br.ReadBytes(768); } } public byte GetWorldRegionByBlock(int blockId) => regionsBuffer[blockId]; public byte GetWorldRegionBySegmentPosition(int x, int y) { var regionIndex = y * 32 + x; if (regionIndex >= regionsBuffer.Length || regionIndex < 0) return 255; else return regionsBuffer[y * 32 + x]; // I had got an exception here. For out of range. //Ok- fixed } #endregion Section 2 - world map regions #region Section 4 - Encounter pointer private const int SEC4_ENC_PER_CHUNK = 8; //there are 8 scene.out pointers per one block/entry public ushort[][] Encounters { get; private set; } private void Section4() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[4 - 1], SeekOrigin.Begin); var encounterList = new List(); while (true) { var dwordTester = br.ReadUInt32(); if (dwordTester == 0) break; ms.Seek(-4, SeekOrigin.Current); var sceneOutPointers = new ushort[SEC4_ENC_PER_CHUNK]; for (var i = 0; i < SEC4_ENC_PER_CHUNK; i++) sceneOutPointers[i] = br.ReadUInt16(); encounterList.Add(sceneOutPointers); } Encounters = encounterList.ToArray(); } } public ushort[] GetEncounters(int pointer) => Encounters[pointer]; #endregion Section 4 - Encounter pointer #region Section 6 - Encounter pointer (Lunar Cry) public ushort[][] EncountersLunar { get; private set; } private void Section6() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[6 - 1], SeekOrigin.Begin); var encounterList = new List(); while (true) { var dwordTester = br.ReadUInt32(); if (dwordTester == 0) break; ms.Seek(-4, SeekOrigin.Current); var sceneOutPointers = new ushort[SEC4_ENC_PER_CHUNK]; for (var i = 0; i < SEC4_ENC_PER_CHUNK; i++) sceneOutPointers[i] = br.ReadUInt16(); encounterList.Add(sceneOutPointers); } EncountersLunar = encounterList.ToArray(); } } public ushort[] GetEncountersLunar(int pointer) => EncountersLunar[pointer]; #endregion Section 6 - Encounter pointer (Lunar Cry) #region Section 8 - World map to field public enum worldScriptOpcodes : ushort { Mode1 = 0xFF01, flagBelow = 0xFF02, //word 2036BDE, on disc3 it's 0xECE flagAbove = 0xFF03, //see above Mode2 = 0xFF04, //probably so-called null animation EndZone = 0xFF05, //this splits the conditions- always at the end or in the middle of two zones SetRegionId = 0xFF06, //for example 283 (If I remember correctly) for Balamb playerPositionUnk = 0xFF07, //probably X and Y for worldmap wm2fieldEntryId = 0xFF08, CheckForVehicle = 0xFF09, Set1 = 0xFF0A, Set3If1OrNot4 = 0xFF0B, Set3IfNot2or5 = 0xFF0C, Set6IfNot2Or5 = 0xFF0D, squallXlocAbove = 0xFF0F, squallZLocAbove = 0xFF10, squallXlocBelow = 0xFF11, squallZlocBelow = 0xFF12, SetTexture = 0xFF13, ScriptEnd = 0xFF16, setUnk2 = 0xFF17, unkBelov2 = 0xFF1A, ReturnMinusOne = 0xFF1E, //return -1 checkVehicleArgument = 0xFF25, //checks argument for vehicle Id } public struct worldMapScript { public worldScriptOpcodes opcode; public int argument; } public struct section8WarpZone { public int field; public bool bAlreadySetField; //only for openviii and testing purpouses- based on conditions one thing can //point to different fields. This is to make sure only first fieldId is set public int segmentId; public worldMapScript[] conditions; } public section8WarpZone[] section8WarpZones; /// /// Section is responsible for world map to field transition and works as a factor of conditions /// You have to read the header of 0xFF01 and read conditions up to 0xFF16 /// The game engine checks given condition and returns TRUE or FALSE. If something fails, then it /// moves forward with checking. Currently we know wm2field pointer, vehicle conditions and /// that's more or less all we know /// public void Section8() { var localZones = new List(); MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[8 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); for (var i = 0; i < innerSec.Length; i++) { ms.Seek(sectionPointers[8 - 1] + innerSec[i], SeekOrigin.Begin); short Mode = 0; Mode = (short)br.ReadUInt32(); var localZone = new section8WarpZone { field = -1 }; var conditions = new List(); while (true) { var opcode = (worldScriptOpcodes)br.ReadUInt16(); var argument = br.ReadUInt16(); if (opcode == worldScriptOpcodes.ScriptEnd) //?? maybe { localZone.conditions = conditions.ToArray(); localZones.Add(localZone); break; } switch (opcode) { case worldScriptOpcodes.SetRegionId: conditions.Add(new worldMapScript() { argument = argument, opcode = opcode }); localZone.segmentId = argument; break; case worldScriptOpcodes.wm2fieldEntryId: conditions.Add(new worldMapScript() { argument = argument, opcode = opcode }); localZone.field = argument; break; default: conditions.Add(new worldMapScript() { argument = argument, opcode = opcode }); break; } } } } section8WarpZones = localZones.ToArray(); } #endregion Section 8 - World map to field #region Section 9 - Field to World map public Vector3[] fieldToWorldMapLocations; private void Section9() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[9 - 1], SeekOrigin.Begin); var sectionSize = sectionPointers[10 - 1] - sectionPointers[9 - 1] - 4; var entriesCount = sectionSize / 12; fieldToWorldMapLocations = new Vector3[entriesCount]; for (var i = 0; i < entriesCount; i++) { var x = br.ReadInt32(); var z = br.ReadInt32(); int y = br.ReadInt16(); fieldToWorldMapLocations[i].X = Extended.ConvertVanillaWorldXAxisToOpenVIII(x); fieldToWorldMapLocations[i].Y = Extended.ConvertVanillaWorldYAxisToOpenVIII(y); fieldToWorldMapLocations[i].Z = Extended.ConvertVanillaWorldZAxisToOpenVIII(z); ms.Seek(2, SeekOrigin.Current); } } } #endregion Section 9 - Field to World map //#region Section 10 - [UNKNOWN]/ Scripts //public static worldMapScript[][] section10Scripts; ///// ///// This file follows some schema of 0xFF01->0xFF04 ///// //private void Section10() //{ // List> scripts = new List>(); // MemoryStream ms; // using (BinaryReader br = new BinaryReader(ms = new MemoryStream(buffer))) // { // ms.Seek(sectionPointers[8 - 1], SeekOrigin.Begin); // int[] innerSec = GetInnerPointers(br); // for (int i = 0; i < innerSec.Length; i++) // { // ms.Seek(sectionPointers[8 - 1] + innerSec[i], SeekOrigin.Begin); // short Mode = 0; // Mode = (short)br.ReadUInt32(); // List localScript = new List(); // while (true) // { // worldScriptOpcodes opcode = (worldScriptOpcodes)br.ReadUInt16(); // ushort argument = br.ReadUInt16(); // if (opcode == worldScriptOpcodes.EndZone) //?? maybe // { // scripts.Add(localScript); // break; // } // localScript.Add(new worldMapScript() { argument = argument, opcode = opcode }); // } // } // } // section10Scripts = scripts.Select(x => x.ToArray()).ToArray(); //} //#endregion Section 10 - [UNKNOWN]/ Scripts #region Section 11 - [UNKNOWN] public Vector3[] sec11Locations; private void Section11() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[11 - 1], SeekOrigin.Begin); var entriesCount = sectionPointers[12 - 1] - sectionPointers[11 - 1] - 4; entriesCount /= 16; entriesCount--; //first entry is null ms.Seek(16, SeekOrigin.Current); //pass first entry, it's null sec11Locations = new Vector3[entriesCount]; for(var i = 0; i> 16; v87 = 0; sub_5450D0(v2, v13, (unsigned __int8)v90, (_BYTE)v90 != 80 ? 0 : 4, (int)&v87, (int)&v83); */ } #endregion Section 11 - [UNKNOWN] #region Section 14 - Side quest strings private FF8String[] sideQuestDialogues; private void Section14() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[14 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); sideQuestDialogues = new FF8String[innerSec.Length]; for (var i = 0; i < innerSec.Length; i++) { ms.Seek(sectionPointers[14 - 1] + innerSec[i], SeekOrigin.Begin); sideQuestDialogues[i] = Extended.GetBinaryString(br); } } } public FF8String GetSection14Text(int index) => sideQuestDialogues[index]; #endregion Section 14 - Side quest strings #region Section 16 - World map objects and vehicles private struct s16Model { public ushort cTriangles; public ushort cQuads; public ushort texPage; public ushort cVerts; public s16Triangle[] triangles; public s16Quad[] quads; public Vector4[] vertices; } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 12)] private struct s16Triangle { public byte A, B, C; public byte semitransp; public byte ua, va; public byte ub, vb; public byte uc, vc; public ushort clut; } [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)] private struct s16Quad { public byte A, B, C, D; public byte ua, va; public byte ub, vb; public byte uc, vc; public byte ud, vd; public ushort clut; public byte semitransp; public byte unk; } private s16Model[] s16Models; private const float s16Scale = 16f; private void Section16() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[16 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); s16Models = new s16Model[innerSec.Length]; for (var i = 0; i < innerSec.Length; i++) { ms.Seek(sectionPointers[16 - 1] + innerSec[i], SeekOrigin.Begin); s16Models[i].cTriangles = br.ReadUInt16(); s16Models[i].cQuads = br.ReadUInt16(); s16Models[i].texPage = br.ReadUInt16(); s16Models[i].cVerts = br.ReadUInt16(); s16Models[i].triangles = new s16Triangle[s16Models[i].cTriangles]; s16Models[i].quads = new s16Quad[s16Models[i].cQuads]; s16Models[i].vertices = new Vector4[s16Models[i].cVerts]; for (var n = 0; n < s16Models[i].cTriangles; n++) s16Models[i].triangles[n] = Extended.ByteArrayToStructure(br.ReadBytes(Marshal.SizeOf(typeof(s16Triangle)))); for (var n = 0; n < s16Models[i].cQuads; n++) s16Models[i].quads[n] = Extended.ByteArrayToStructure(br.ReadBytes(Marshal.SizeOf(typeof(s16Quad)))); for (var n = 0; n < s16Models[i].cVerts; n++) s16Models[i].vertices[n] = new Vector4( br.ReadInt16() / s16Scale, br.ReadInt16() / s16Scale, br.ReadInt16() / s16Scale, br.ReadUInt16() ); } } } /// /// Gets simple geometry for vehicles. Takes the objectId, local Vector3 and quaternion for rotation. Returns tuple with data for /// renderer and byte[] with clut Ids /// /// /// /// /// texture resolution for custom UV recalculation /// texture vector for origin (x-832 based) and y zero based for custom UV recalculation /// public Tuple GetVehicleGeometry(int objectId, Vector3 localTranslation, Quaternion rotation, Vector2? textureResolution = null, Vector2? textureOriginVector = null) { //This ones are simple- static meshes that are translated only by basic localTranslation and quaternion. Nothing much var vptList = new List(); var vptTextureIndexList = new List(); //step 1. grab triangles if (objectId > s16Models.Length) return new Tuple(new VertexPositionTexture[0], new byte[0]); //error var Model = s16Models[objectId]; var texWidth = 256f; var texHeight = 256f; byte localXadd = 0; byte localYadd = 0; if (textureResolution != null) { texWidth = textureResolution.Value.X; texHeight = textureResolution.Value.Y; }; if (textureOriginVector != null) { localXadd = (byte)textureOriginVector.Value.X; localYadd = (byte)textureOriginVector.Value.Y; } for (var i = 0; i < Model.cTriangles; i++) { var a = Extended.ShrinkVector4ToVector3(Model.vertices[Model.triangles[i].A], true); var b = Extended.ShrinkVector4ToVector3(Model.vertices[Model.triangles[i].B], true); var c = Extended.ShrinkVector4ToVector3(Model.vertices[Model.triangles[i].C], true); a = Vector3.Transform(a, Matrix.CreateFromQuaternion(rotation)); b = Vector3.Transform(b, Matrix.CreateFromQuaternion(rotation)); c = Vector3.Transform(c, Matrix.CreateFromQuaternion(rotation)); a += localTranslation; b += localTranslation; c += localTranslation; vptList.Add(new VertexPositionTexture( a, new Vector2( (Model.triangles[i].ua - localXadd) / texWidth, (Model.triangles[i].va - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( b, new Vector2( (Model.triangles[i].ub - localXadd) / texWidth, (Model.triangles[i].vb - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( c, new Vector2( (Model.triangles[i].uc - localXadd) / texWidth, (Model.triangles[i].vc - localYadd) / texHeight) )); vptTextureIndexList.Add((byte)Model.triangles[i].clut); vptTextureIndexList.Add((byte)Model.triangles[i].clut); vptTextureIndexList.Add((byte)Model.triangles[i].clut); } for (var i = 0; i < Model.cQuads; i++) { var a = Extended.ShrinkVector4ToVector3(Model.vertices[Model.quads[i].A], true); var b = Extended.ShrinkVector4ToVector3(Model.vertices[Model.quads[i].B], true); var c = Extended.ShrinkVector4ToVector3(Model.vertices[Model.quads[i].C], true); var d = Extended.ShrinkVector4ToVector3(Model.vertices[Model.quads[i].D], true); a = Vector3.Transform(a, Matrix.CreateFromQuaternion(rotation)); b = Vector3.Transform(b, Matrix.CreateFromQuaternion(rotation)); c = Vector3.Transform(c, Matrix.CreateFromQuaternion(rotation)); d = Vector3.Transform(d, Matrix.CreateFromQuaternion(rotation)); a += localTranslation; b += localTranslation; c += localTranslation; d += localTranslation; vptList.Add(new VertexPositionTexture( a, new Vector2( (Model.quads[i].ua - localXadd) / texWidth, (Model.quads[i].va - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( b, new Vector2( (Model.quads[i].ub - localXadd) / texWidth, (Model.quads[i].vb - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( d, new Vector2( (Model.quads[i].ud - localXadd) / texWidth, (Model.quads[i].vd - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( a, new Vector2( (Model.quads[i].ua - localXadd) / texWidth, (Model.quads[i].va - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( c, new Vector2( (Model.quads[i].uc - localXadd) / texWidth, (Model.quads[i].vc - localYadd) / texHeight) )); vptList.Add(new VertexPositionTexture( d, new Vector2( (Model.quads[i].ud - localXadd) / texWidth, (Model.quads[i].vd - localYadd) / texHeight) )); vptTextureIndexList.Add((byte)Model.quads[i].clut); vptTextureIndexList.Add((byte)Model.quads[i].clut); vptTextureIndexList.Add((byte)Model.quads[i].clut); vptTextureIndexList.Add((byte)Model.quads[i].clut); vptTextureIndexList.Add((byte)Model.quads[i].clut); vptTextureIndexList.Add((byte)Model.quads[i].clut); } return new Tuple(vptList.ToArray(), vptTextureIndexList.ToArray()); } public int GetVehicleModelsCount() => s16Models.Length; #endregion Section 16 - World map objects and vehicles #region Section 17 + 41 - World map texture animations for beach[17] and water[41] /* * Section 17 is responsible for beach texture animations. It's a double file- first it contains chunks. Every chunk contains * animation frames information. * There's also new texture structure; I haven't seen such structures earlier. Palette is grabbed from section38 */ //MODDING NOTE: Thanks to OpenVIII you can now create bigger chunks than 64x64/64x32 without worrying about SSIGPU VRAM! [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 8)] public struct textureAnimation { /// /// Unknown /// public byte unk; /// /// Animation speed/timeout between frames- usually 0x20. 1 is the fastest. 0 is invalid. 0xFF is long /// public byte animTimeout; /// /// Frames Count- controls how many frames are valid. Usually 4 /// public byte framesCount; /// /// If true then frames loop backward. For example 0-1-2-3-2-1-0. If false, then 0-1-2-3-0-1-2-3 /// public byte bLooping; /// /// Unused in openviii. It is starting VRAM offset for this texture to be stored /// public ushort vramOrigX; /// /// Unused in openviii. It is starting VRAM offset for this texture to be stored /// public ushort vramOrigY; /// /// Contains texture data for given frame /// /// Did you know that there's no way to tell C# to not marshal one field? public Texture2D[] framesTextures; /// /// Contains palette data for given frame- because section41 is palette (version 17) /// public Color[][] framesPalette; /// /// OpenVIII helper value- if true then index is incrementing- if false then index is decrementing /// public bool bIncrementing; /// /// OpenVIII helper value- holds current frame index /// public int currentAnimationIndex; /// /// OpenVIII helper value- holds total deltaTime to be used with timeout calculation /// public TimeSpan deltaTime; } private textureAnimation[] beachAnimations; private textureAnimation[] waterAnimations; public void Section17() { if (Memory.Graphics?.GraphicsDevice != null) { int[] innerPointers; MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[17 - 1], SeekOrigin.Begin); innerPointers = GetInnerPointers(br); BeachAnimations = new textureAnimation[innerPointers.Length]; for (var i = 0; i < innerPointers.Length; i++) BeachAnimations[i] = textureAnimation_ParseBlock(sectionPointers[17 - 1] + innerPointers[i], i, ms, br); //for (int i = 0; i < beachAnimations.Length; i++) // for (int n = 0; n < beachAnimations[i].framesCount; n++) // Extended.DumpTexture(beachAnimations[i].framesTextures[n], $"D:\\b_{i}_{n}.png"); } } } public void Section41() { int[] innerPointers; MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[41 - 1], SeekOrigin.Begin); innerPointers = GetInnerPointers(br); waterAnimations = new textureAnimation[innerPointers.Length]; for (var i = 0; i < innerPointers.Length; i++) waterAnimations[i] = textureAnimation_ParseBlock(sectionPointers[41 - 1] + innerPointers[i], -1, ms, br); } } private const int sec17_imageHeaderSize = 12; //dword;dword; sizeDword /// /// Valid for section 41 and 17! /// /// /// -1 means section41 /// /// /// private textureAnimation textureAnimation_ParseBlock(int offset, int texturePointer, MemoryStream ms, BinaryReader br) { ms.Seek(offset, SeekOrigin.Begin); //beachAnimation animation = Extended.ByteArrayToStructure(br.ReadBytes(8)); var animation = new textureAnimation() //not using Extension because Marshalling doesn't go well with [][] even //though it's not really even used! { unk = br.ReadByte(), animTimeout = br.ReadByte(), framesCount = br.ReadByte(), bLooping = br.ReadByte(), vramOrigX = br.ReadUInt16(), vramOrigY = br.ReadUInt16() }; if (texturePointer == -1) ms.Seek(4, SeekOrigin.Current); //there are two WORD's that feel like they have again the palette X and Y but why?? var preImagePosition = (uint)ms.Position; var imagePointers = new uint[animation.framesCount]; Color[] palette = null; Color[][] customPalette = null; if (texturePointer != -1) palette = GetWorldMapTexturePalette(texturePointer == 0 ? Section38_textures.beach : Section38_textures.beachE, 0); for (var i = 0; i < animation.framesCount; i++) imagePointers[i] = br.ReadUInt32() + preImagePosition; Texture2D[] animationFrames = null; if (texturePointer != -1) animationFrames = new Texture2D[imagePointers.Length]; else { customPalette = new Color[imagePointers.Length][]; animationFrames = new Texture2D[imagePointers.Length]; } for (var i = 0; i < animation.framesCount; i++) { ms.Seek(imagePointers[i], SeekOrigin.Begin); var unknownHeader = br.ReadUInt32(); var unknownHeader_ = br.ReadUInt32(); if (unknownHeader != 18 && unknownHeader != 17) //I don't know why 18 [those are some version or flags?] throw new Exception("wmset::section17::texturePointerHeader != 17 or 18"); if (unknownHeader_ != 1) //I don't know why 1 throw new Exception("wmset::section17::texturePointerHeader+4 != 1"); _ = br.ReadUInt32() - sec17_imageHeaderSize; //imageSize, but doesn't really matter here _ = br.ReadUInt32(); //unknown var width = (ushort)(br.ReadUInt16() * 2); var height = br.ReadUInt16(); Texture2D texture; Color[] texBuffer; if (texturePointer != -1) { texture = new Texture2D(Memory.Graphics.GraphicsDevice, width, height, false, SurfaceFormat.Color); texBuffer = new Color[width * height]; //32bpp because Color is ARGB byte : struct if (palette.Length == 16) { for (var m = 0; m < texBuffer.Length; m += 2) { var b = br.ReadByte(); texBuffer[m] = palette[b & 0xF]; texBuffer[m + 1] = palette[b >> 4]; } } else for (var m = 0; m < texBuffer.Length; m++) { var b = br.ReadByte(); texBuffer[m] = palette[b]; } texture.SetData(texBuffer); animationFrames[i] = texture; } else { width /= 2; customPalette[i] = new Color[width]; //in sec41 width is the number of colours for (var m = 0; m < width; m++) { var color = br.ReadUInt16(); customPalette[i][m] = Texture_Base.ABGR1555toRGBA32bit(color); } } } animation.framesTextures = animationFrames; animation.framesPalette = customPalette; return animation; } public textureAnimation GetBeachAnimation(int animationId) => BeachAnimations[animationId]; /// /// Gets chunk from beachAnim atlas (because they are structured 2x2) /// /// index of the animation wanted- there are two beach anim sets and one unknown /// naturally the frame/keyframe of the animation /// public Texture2D GetBeachAnimationTextureFrame(int animationId, int frameId) => BeachAnimations[animationId].framesTextures[frameId]; public Color[] GetWaterAnimationPalettes(int animId, int frameId) => waterAnimations[animId].framesPalette[frameId].ToArray(); public Texture2D GetWaterAnimationTextureFrame(int animationId, int frameId) => WaterAnimations[animationId].framesTextures[frameId]; internal textureAnimation[] BeachAnimations { get => beachAnimations; set => beachAnimations = value; } internal textureAnimation[] WaterAnimations { get => waterAnimations; set => waterAnimations = value; } #endregion Section 17 + 41 - World map texture animations for beach[17] and water[41] #region Section 32 - World map location names private FF8String[] locationsNames; private void Section32() { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[32 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); locationsNames = new FF8String[innerSec.Length]; for (var i = 0; i < locationsNames.Length; i++) { ms.Seek(sectionPointers[32 - 1] + innerSec[i], SeekOrigin.Begin); locationsNames[i] = Extended.GetBinaryString(br); } } } public FF8String GetLocationName(int index) => locationsNames[index]; public int GetlocationNamesLength => locationsNames.Length; #endregion Section 32 - World map location names #region Section 33 - Sky and ambient colour [StructLayout(LayoutKind.Sequential,Pack =1, Size =52)] public struct sec33SkyEntry { public int positionX; public int positionY; public int positionZ; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] shadows; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] vehicles; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] skyGradientTop; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] skyGradientCenter; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] skyGradientBottom; public byte unk1_1; public byte unk1_2; //unk1_2 - R value [0-127] public byte unk1_3; public byte unk1_4; //unk1_4 = R value [0-127] public byte unk2_1; public byte unk2_2; //unk2_2 - G value [0-127] public byte unk2_3; public byte unk2_4; //unk2_4 = G value [0-127] public byte unk3_1; public byte unk3_2; //unk3_2 - B value [0-127] public byte unk3_3; public byte unk3_4; //unk3_4 = B value [0-127] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] unk4; //wrong place- one of the byte was switch for full colorization of wm [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] unk5; //wrong place- one of the byte was switch for full colorization of wm public Color GetShadowsColor() => new Color(shadows[0], shadows[1], shadows[2], (byte)255); public Color GetVehiclesColor() => new Color(vehicles[0], vehicles[1], vehicles[2], (byte)255); public Color GetTopBGColor() => new Color(skyGradientTop[0], skyGradientTop[1], skyGradientTop[2], (byte)255); public Color GetCenterBGColor() => new Color(skyGradientCenter[0], skyGradientCenter[1], skyGradientCenter[2], (byte)255); public Color GetBottomBGColor() => new Color(skyGradientBottom[0], skyGradientBottom[1], skyGradientBottom[2], (byte)255); public Vector3 GetLocation() => new Vector3() { X = Extended.ConvertVanillaWorldXAxisToOpenVIII(positionX), Y = Extended.ConvertVanillaWorldYAxisToOpenVIII(positionZ), Z = Extended.ConvertVanillaWorldZAxisToOpenVIII(positionY) }; } public sec33SkyEntry[] skyColors; private void Section33() { using (var ms = new MemoryStream(buffer)) //didn't know you can skip braces with using using (var br = new BinaryReader(ms)) { ms.Seek(sectionPointers[33 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); var skyEntries = new List(); for (var i = 0; i < innerSec.Length; i++) { ms.Seek(sectionPointers[33 - 1] + innerSec[i], SeekOrigin.Begin); skyEntries.Add(Extended.ByteArrayToStructure(br.ReadBytes(52))); } skyColors = skyEntries.ToArray(); } } #endregion #region Section 38 - World map textures archive /// /// Section 38: World map textures archive /// /// private List sec38_textures; private List sec38_pals; //because other sections rely on palettes of wmset.38 private TIM2 waterTim; private TIM2 waterTim2; private TIM2 waterTim3; private TIM2 waterTim4; public enum Section38_textures { wmtex0, wmtex1, wmtex2, wmtex3, wmtex4, wmtex5, wmtex6, wmtex7, waterTex, moon, clouds, worldmapMinimap, wmunk12, wmunk13, wmfx14, wmfx_bush, waterTex2, waterTex3, waterTex4, waterfall, waterTex5, beach, beachE, waterTex6, minimapPointer, minimapFullScreenPointer, wmfx26, wmunk27, wmfx28, wmunk29, wmunk30, wmunk31, wmfx32, wmunk33, shadowBig, magicBarrier } private void Section38() { sec38_pals = new List(); MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[38 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); sec38_textures = new List(); for (var i = 0; i < innerSec.Length; i++) { var tim = new TIM2(buffer, (uint)(sectionPointers[38 - 1] + innerSec[i])); if (i == (int)Section38_textures.waterTex2) waterTim = tim; if (i == (int)Section38_textures.waterTex3) waterTim2 = tim; if (i == (int)Section38_textures.waterTex4) waterTim3 = tim; if (i == (int)Section38_textures.waterfall) waterTim4 = tim; if (tim.GetBytesPerPixel == 4) if (tim.GetClutSize != (tim.GetClutCount * tim.GetColorsCountPerPalette)) //broken header, force our own values { tim.ForceSetClutColors(16); tim.ForceSetClutCount((ushort)(tim.GetClutSize / 16)); } sec38_textures.Add(new TextureHandler[tim.GetClutCount]); sec38_pals.Add(new Color[tim.GetClutCount][]); for (ushort k = 0; k < sec38_textures[i].Length; k++) { var table = tim.GetPalette(k); sec38_pals[i][k] = table; sec38_textures[i][k] = TextureHandler.Create($"wmset_tim38_{(i + 1).ToString("D2")}.tim", tim, k, table); } } } CreateWaterTextureAtlas(); } private const int WATERBLOCKTEXW = 64; //should be probably dynamic to conform mods private const int WATERBLOCKTEXH = 64; private Texture2D waterAtlas; /// /// Creates atlas with WaterBlockWidth*4 px based on 8BPP TIMs (for animated WM). Therefore user can replace /// individual texture without breaking the atlas as it's created basing on TextureHandler /// but their size must conform 4x2 layout where each block has the SAME resolution /// private void CreateWaterTextureAtlas() { /* * for animated water materials there's ANOTHER water texture that is NOT watertex, but is rather built from various * blocks in independent TIMs- they are NOT 4BPP but 8BPP- although they are not correctly ordered- once again * their VRAM x and y kick-in, but we got it covered- the blocks are 4x4 (64px per block). The blocks order is: * 0 64 128 192 * 0 [24] [17] [22] [23] * 64 [18] [19] [20] [21] */ if (Memory.Graphics?.GraphicsDevice != null) { waterAtlas = new Texture2D(Memory.Graphics.GraphicsDevice, WATERBLOCKTEXW << 2, WATERBLOCKTEXH << 1, false, SurfaceFormat.Color); //64<<2 is 256 WaterTextureAtlasPutChunk(Section38_textures.waterTex6, 0, 0); // 0 0 WaterTextureAtlasPutChunk(Section38_textures.waterTex2, WATERBLOCKTEXW, 0); // 64 0 WaterTextureAtlasPutChunk(Section38_textures.beach, WATERBLOCKTEXW * 2, 0); // 128 0 WaterTextureAtlasPutChunk(Section38_textures.beachE, WATERBLOCKTEXW * 3, 0); // 192 0 WaterTextureAtlasPutChunk(Section38_textures.waterTex3, 0, WATERBLOCKTEXH); // 0 64 WaterTextureAtlasPutChunk(Section38_textures.waterTex4, WATERBLOCKTEXW, WATERBLOCKTEXH); // 64 64 WaterTextureAtlasPutChunk(Section38_textures.waterfall, WATERBLOCKTEXW * 2, WATERBLOCKTEXH); // 128 64 WaterTextureAtlasPutChunk(Section38_textures.waterTex5, WATERBLOCKTEXW * 3, WATERBLOCKTEXH); // 192 64 Extended.DumpTexture(waterAtlas, "D:/test.png"); } } private void WaterTextureAtlasPutChunk(Section38_textures textureIndex, int x, int y) { var atlasChunk = (Texture2D)GetWorldMapTexture(textureIndex, 0); //there's only one clut var chunkBuffer = new byte[WATERBLOCKTEXW * WATERBLOCKTEXH * 4]; atlasChunk.GetData(level: 0, rect: new Rectangle(0, 0, WATERBLOCKTEXW, WATERBLOCKTEXH), chunkBuffer, 0, chunkBuffer.Length); waterAtlas.SetData(0, new Rectangle(x, y, WATERBLOCKTEXW, WATERBLOCKTEXH), chunkBuffer, 0, chunkBuffer.Length); } private void UpdateWaterTextureAtlasChunk(Texture2D animatedChunk, int x, int y) { var chunkBuffer = new byte[WATERBLOCKTEXW * WATERBLOCKTEXH * 4]; animatedChunk.GetData(level: 0, rect: new Rectangle(0, 0, WATERBLOCKTEXW, WATERBLOCKTEXH), chunkBuffer, 0, chunkBuffer.Length); waterAtlas.SetData(0, new Rectangle(x, y, WATERBLOCKTEXW, WATERBLOCKTEXH), chunkBuffer, 0, chunkBuffer.Length); } /// /// Gets textures from section 38 /// /// /// /// public TextureHandler GetWorldMapTexture(Section38_textures index, int clut) => sec38_textures[(int)index][clut]; /// /// Gets the pre-made atlas from TIMs- do not change to TextureHandler! This atlas /// is final product from moddable TextureHandlers- it shouldn't be directly changed /// /// public Texture2D GetWorldMapWaterTexture() => waterAtlas; public Color[] GetWorldMapTexturePalette(Section38_textures index, int clut) => sec38_pals[(int)index][clut]; public void UpdateWorldMapWaterTexturePaletteForAnimation(int index, Color[] palette) { if (waterTim == null) return; Texture2D animatedChunk = null; int x = 0, y = 0; switch (index) { case 0: animatedChunk = waterTim2.GetTexture(palette); x = 0; y = WATERBLOCKTEXH; break; case 3: animatedChunk = waterTim.GetTexture(palette); x = WATERBLOCKTEXW; y = 0; break; case 2: animatedChunk = waterTim4.GetTexture(palette); x = WATERBLOCKTEXW * 2; y = WATERBLOCKTEXH; break; case 1: animatedChunk = waterTim3.GetTexture(palette); x = WATERBLOCKTEXW; y = WATERBLOCKTEXH; break; } UpdateWaterTextureAtlasChunk(animatedChunk, x, y); } #endregion Section 38 - World map textures archive #region Section 39 - Textures of roads, train tracks and bridges private const int SEC39_VRAM_STARTX = 832; //this is beginning of origX to map to one texture private const int SEC39_VRAM_STARTY = 256; //used to map VRAM, but here it's used to create new atlas private const int VRAM_TEXBLOCKWIDTH = 256; //wm faces ask VRAM, not texture, so the block is 256px in VRAM + additional chunks from other files that we deal normally as Tex2D[] private const int VRAM_TEXBLOCKHEIGHT = 256; //see above private const int VRAM_BLOCKSIZE = 32; // =VRAM_BLOCKSTEP*4 - one origX of 16 is actually 16/2=8*32=finalXorig private const int VRAM_BLOCKSTEP = 8; private TextureHandler sec39_texture; /// /// Section 39: Textures of roads, train tracks and bridge /// private void Section39() { if (Memory.Graphics?.GraphicsDevice != null) { MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[39 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); var sec39_texture = new Texture2D(Memory.Graphics.GraphicsDevice, VRAM_TEXBLOCKWIDTH, VRAM_TEXBLOCKHEIGHT, false, SurfaceFormat.Color); for (var i = 0; i < innerSec.Length; i++) { var tim = new TIM2(buffer, (uint)(sectionPointers[39 - 1] + innerSec[i])); var atlasChunk = tim.GetTexture(0); var chunkBuffer = new byte[atlasChunk.Width * atlasChunk.Height * 4]; atlasChunk.GetData(chunkBuffer, 0, chunkBuffer.Length); var newX = tim.GetOrigX - SEC39_VRAM_STARTX; var newY = tim.GetOrigY - SEC39_VRAM_STARTY; newX = (newX / VRAM_BLOCKSTEP) * VRAM_BLOCKSIZE; sec39_texture.SetData(0, new Rectangle(newX, newY, atlasChunk.Width, atlasChunk.Height), chunkBuffer, 0, chunkBuffer.Length); } this.sec39_texture = TextureHandler.Create($"wmset_tim39.tim", new Texture2DWrapper(sec39_texture), 0, null); //sec39_texture = TextureHandler.Create($"wmset_tim{(i + 1).ToString("D2")}.tim", new TIM2(buffer, (uint)(sectionPointers[39 - 1] + innerSec[i])), k, null); } } } public enum Section39_Textures { train, bridgeTrack, trainMetal, trainMetalCrossTrain, TrainCrossTrainMetal, asphalt, dirtWay, dirtWay2, dirtWay3, desertWay, desertWay2, desertWay3, unknownMetal } /// /// Gets textures from Section39 /// /// public Texture2D GetRoadsMiscTextures() => (Texture2D)sec39_texture; #endregion Section 39 - Textures of roads, train tracks and bridges #region Section 42 - objects and vehicles textures private const int SEC42_VRAM_STARTX = 832; //this is beginning of origX to map to one texture private TextureHandler[][] vehicleTextures; private Vector2[] timOrigHolder; public enum VehicleTextureEnum { BalambGarden, BalambHalo, TrainA_locomotive, TrainA_cab, TrainB_locomotive, TrainB_cab, TrainC_locomotive, TrainC_cab, TrainC_cab2, Car1, Car2, Car3, TrainRoadBlockade, unk2, Vessel, GalbadiaGarden, GalbadiaHalo, Car4, Car5, Car6, Car7, Car8, Car9, Car10, WhiteSeed, LunaticPandora, LunaticPandora2, LunaticPanodra_inside, UltimeciaGate, UltimeciaBarrier, Cactuar, RoadBlockade, UltimeciaBarrier2 } private void Section42() { var vehTextures = new List(); var timOriginHolderList = new List(); //VRAM atlas, holds X and Y origins for atlasing- here for calculating new UV MemoryStream ms; using (var br = new BinaryReader(ms = new MemoryStream(buffer))) { ms.Seek(sectionPointers[42 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); for (var i = 0; i < innerSec.Length; i++) { var tim = new TIM2(buffer, (uint)(sectionPointers[42 - 1] + innerSec[i])); timOriginHolderList.Add(new Vector2((tim.GetOrigX - SEC42_VRAM_STARTX) * 4, tim.GetOrigY)); vehTextures.Add(new TextureHandler[tim.GetClutCount]); for (ushort k = 0; k < vehTextures[i].Length; k++) vehTextures[i][k] = TextureHandler.Create($"wmset_tim42_{(i + 1).ToString("D2")}.tim", tim, k, null); } } vehicleTextures = vehTextures.ToArray(); timOrigHolder = timOriginHolderList.ToArray(); } public TextureHandler GetVehicleTexture(int index, int clut) => vehicleTextures[index][clut]; public TextureHandler GetVehicleTexture(VehicleTextureEnum index, int clut) => vehicleTextures[(int)index][clut]; /// /// Gets X and Y tim origin (psx VRAM) for recalculating UV /// /// /// /// public Vector2 GetVehicleTextureOriginVector(VehicleTextureEnum index, int clut) => timOrigHolder[(int)index]; /// /// Gets X and Y tim origin (psx VRAM) for recalculating UV /// /// /// /// public Vector2 GetVehicleTextureOriginVector(int index, int clut) => timOrigHolder[index]; #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. waterAtlas?.Dispose(); disposedValue = true; } } // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. // ~wmset() { // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. // Dispose(false); // } // This code added to correctly implement the disposable pattern. public void Dispose() => // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true);// TODO: uncomment the following line if the finalizer is overridden above.// GC.SuppressFinalize(this); #endregion IDisposable Support #endregion Section 42 - objects and vehicles textures } } /* * Snippet for section w/ inner pointers * * using (MemoryStream ms = new MemoryStream(buffer)) using (BinaryReader br = new BinaryReader(ms)) { ms.Seek(sectionPointers[8 - 1], SeekOrigin.Begin); var innerSec = GetInnerPointers(br); } */