1 /// Franks PC Shapes (Textures)
2 ///
3 /// See_Also: $(UL
4 ///   $(LI <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH">Franks PC Shapes</a> (SC4D Encyclopedia))
5 ///   $(LI <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format">FSH Format</a> (SC4D Encyclopedia))
6 /// )
7 ///
8 /// Authors: Chance Snow
9 /// Copyright: Copyright © 2024 Chance Snow. All rights reserved.
10 /// License: MIT License
11 module dbpf.files.fsh;
12 
13 import std.conv : to;
14 import std.exception : enforce;
15 import std.string : representation;
16 
17 ///
18 enum DirectoryId {
19   /// Building textures
20   building = "G354".representation,
21   /// Network textures, Sim textures, Sim heads, Sim animations, Trees, props, Base textures, Misc. colors
22   generic = "G264".representation,
23   /// 3D Animation textures (e.g. the green rotating diamond in `loteditor.dat`)
24   _3dAnimation = "G266".representation,
25   /// Dispatch marker textures
26   dispatch = "G290".representation,
27   /// Small Sim texture, Network Transport Model textures (trains, etc.)
28   simThumbOrNetworkModel = "G315".representation,
29   /// UI Editor textures
30   ui = "GIMX".representation,
31   /// BAT generator texture maps
32   bat = "G344".representation,
33 }
34 
35 ///
36 struct Header {
37   /// Always `SHPI`.
38   static const identifier = "SHPI".representation;
39 align(1):
40   /// Always `SHPI`.
41   ubyte[4] magic = identifier.to!(ubyte[4]);
42   ///
43   uint size;
44   ///
45   uint entryCount;
46   /// See_Also: `DirectoryId`
47   uint directoryId;
48 }
49 
50 static assert(Header.alignof == 1);
51 static assert(Header.sizeof == 16);
52 
53 ///
54 enum EntryName {
55   /// Global palette for 8-bit Indexed Bitmaps.
56   palette = "!pal".representation,
57   /// Buildings, props, network intersections, and terrain textures.
58   zero = "0000".representation,
59   /// Always used for a rail texture, whereas for street/road intersections it's always by instance.
60   rail = "rail".representation,
61   /// First sprite animation entry in a directory.
62   tb2 = "TB2".representation,
63   /// Any sprite animation entries in a directory after TB2.
64   tb3 = "TB3".representation,
65 }
66 
67 ///
68 struct Directory {
69   import dbpf.types : str;
70 align(1):
71   /// Remarks:
72   /// When searching for a global palette for 8-bit bitmaps, the directory entry name for the global palette will
73   /// always '!pal'. Once the '!pal' directory entry has been found, the global palette can be extracted and used for
74   /// any bitmaps that use 8-bit indexed color. If no global palette is found, FSH decoders should look for a local
75   /// palette directly following the indexed bitmap. If no palette is found, then no palette will be created or
76   /// associated with the bitmap.
77   ///
78   /// Most tools, like FSHTool, simply ignore missing palettes and save the bitmap with an empty palette with all
79   /// indices set to black.
80   /// See_Also: `EntryName`
81   str!4 entryName;
82   /// Offset of the entry in the FSH file, in bytes.
83   uint offset;
84 }
85 
86 static assert(Directory.alignof == 1);
87 static assert(Directory.sizeof == 8);
88 
89 /// See_Also: <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format#FSH_Entry_Header">FSH Entry Header</a> (SC4D Encyclopedia)
90 struct EntryHeader {
91   import dbpf.types : int24;
92 
93 align(1):
94   /// Record ID
95   ///
96   /// Logically `AND`ed by `0x7f` for bitmap code or `0x80` to check if the entry is QFS compressed (unused by SC4).
97   ubyte recordId;
98   /// Size of an entry including this header.
99   ///
100   /// Only used if the file contains an attachment or embedded mipmaps. It is zero otherwise.
101   /// Remarks:
102   /// $(P For single images this is usually: `width x height + 0x10h`.)
103   /// $(P
104   ///   For images with embedded mipmaps, this is the total size of the original image, plus all mipmaps, plus the
105   ///   header.
106   /// )
107   /// $(P In either case, it may include additional data as a binary attachment with unknown format.)
108   int24 size;
109   ///
110   ushort width;
111   ///
112   ushort height;
113   ///
114   ushort centerX;
115   ///
116   ushort centerY;
117   ///
118   ushort positionX;
119   ///
120   ushort positionY;
121 }
122 
123 static assert(EntryHeader.alignof == 1);
124 static assert(EntryHeader.sizeof == 16);
125 
126 import std.typecons : Tuple;
127 /// A tuple of an entry's header and its data.
128 /// `data` is either palette or bitmap data.
129 /// Remarks:
130 /// After an entry's `header` is its bitmap, palette, or pixel color data.
131 ///Authors:
132 /// Palettes are generally arrays of 256 colors, each 1 byte. Bitmaps may store their pixel data in one of many ways,
133 /// either raw bitmap pixel data, or they can make use of Microsoft DXTC compressed formats.
134 /// See_Also: $(UL
135 ///   $(LI `EntryHeader`)
136 ///   $(LI <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format#Bitmap_or_Palette_Data">Bitmap data</a> (SC4D Encyclopedia))
137 ///   $(LI <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format#FSH_Entry_Header">FSH Entry Header</a> (SC4D Encyclopedia))
138 /// )
139 alias Entry = Tuple!(EntryHeader, "header", ubyte[], "data");
140 
141 /// FSH images can store their pixel data raw, or they can make use of Microsoft DXTC compressed formats.
142 /// See_Also: <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format#Bitmap_or_Palette_Data">Bitmap data</a> (SC4D Encyclopedia)
143 enum BitmapType : ushort {
144   /// 8-bit indexed
145   ///
146   /// Directly follows bitmap or uses global palette.
147   indexed = 0x7B,
148   /// 32-bit A8R8G8B8
149   a8r8g8b8 = 0x7D,
150   /// 24-bit A0R8G8B8
151   a0r8g8b8 = 0x7F,
152   /// 16-bit A1R5G5B5
153   a1r5g5b5 = 0x7E,
154   /// 16-bit A0R5G6B5
155   a0r5g6b5 = 0x78,
156   /// 16-bit A4R4G4B4
157   a4r4g4b4 = 0x6D,
158   /// DXT3 4x4 packed, 4-bit alpha
159   ///
160   /// 4x4 grid compressed, half-byte per pixel
161   dxt3 = 0x61,
162   /// DXT1 4x4 packed, 1-bit alpha
163   ///
164   /// 4x4 grid compressed, half-byte per pixel
165   dxt1 = 0x60
166 }
167 
168 /// See_Also: <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format#Bitmap_or_Palette_Data">Palette codes</a> (SC4D Encyclopedia)
169 enum Palette : ushort {
170   /// 24-bit DOS
171   dos = 0x22,
172   /// 24-bit
173   _24bit = 0x24,
174   /// 16-bit NFS5
175   nfs5 = 0x29,
176   /// 32-bit
177   _32bit = 0x2A,
178   /// 16-bit
179   _16bit = 0x2D,
180 }
181 
182 /// See_Also: <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format#Bitmap_or_Palette_Data">Text codes</a> (SC4D Encyclopedia)
183 enum Text : ushort {
184   /// Standard Text file
185   text = 0x6F,
186   /// ETXT of arbitrary length with full entry header
187   etxt = 0x69,
188   /// ETXT of 16 bytes or less including the header
189   etxt16 = 0x70,
190   /// Defined Pixel region hot-spot data for image
191   hotspot = 0x7C,
192 }
193 
194 /// A FSH document.
195 /// See_Also: $(UL
196 ///   $(LI `Header`)
197 ///   $(LI `Directory`)
198 ///   $(LI `Entry`)
199 ///   $(LI <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH">Franks PC Shapes</a> (SC4D Encyclopedia))
200 ///   $(LI <a href="https://www.wiki.sc4devotion.com/index.php?title=FSH_Format">FSH Format</a> (SC4D Encyclopedia))
201 /// )
202 alias Fsh = Tuple!(Header, "header", Directory[], "directories", Entry[], "entries");
203 
204 ///
205 Fsh read(ubyte[] file) {
206   import dbpf.files : read;
207   import std.algorithm : startsWith;
208   import std.conv : castFrom, to;
209   import std.typecons : tuple;
210 
211   enforce(file.startsWith(Header.identifier), "Input is not a FSH document.");
212   auto header = file.read!Header();
213   // TODO: Read bitmap entries
214 
215   Fsh result = tuple(
216     header,
217     castFrom!(void[]).to!(Directory[])([]),
218     castFrom!(void[]).to!(Entry[])([]),
219   );
220   return result;
221 }
222 
223 ///
224 ubyte[] write(Fsh document) {
225   import dbpf.files : toBytes;
226   import std.algorithm : copy, map, sum;
227 
228   auto buffer = new ubyte[
229     Header.sizeof +
230     (Directory.sizeof * document.directories.length) +
231     (EntryHeader.sizeof * document.entries.length) +
232     document.entries.map!(entry => EntryHeader.sizeof + entry.header.size.value).sum
233   ];
234 
235   document.header.toBytes.copy(buffer[0..Header.sizeof]);
236   // TODO: Write bitmap entries to the buffer
237 
238   return buffer;
239 }