Skip to content

Commit 88b6896

Browse files
KiritoDvdcvz
andauthored
Merge latest hd texture changes into main repo (#39)
* Merge all font images into one * Merged font texture * Fix font dumping with retro and extension issues * Added debug font generator * Debug features now show on release * Rename conversion methods * Add support for extracting two OTRs * Use spock as right version for asset headers --------- Co-authored-by: David Chavez <david@dcvz.io>
1 parent b75668f commit 88b6896

14 files changed

Lines changed: 511 additions & 111 deletions

File tree

lib/features/create/create_finish/create_finish_viewmodel.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
88
import 'package:flutter/material.dart' hide Image hide Texture;
99
import 'package:flutter_storm/flutter_storm.dart';
1010
import 'package:flutter_storm/flutter_storm_defines.dart';
11-
import 'package:flutter_storm/flutter_storm_bindings.dart';
1211
import 'package:image/image.dart';
1312
import 'package:retro/models/texture_manifest_entry.dart';
1413
import 'package:retro/otr/types/sequence.dart';
@@ -262,7 +261,7 @@ Future<Uint8List?> processJPEG(pair, String textureName) async {
262261
double hByteScale = (image.width / pair.item2.textureWidth) * (texture.textureType.pixelMultiplier / TextureType.RGBA16bpp.pixelMultiplier);
263262
double vPixelScale = (image.height / pair.item2.textureHeight);
264263
texture.setTextureScale(hByteScale, vPixelScale);
265-
texture.fromPNGImage(image);
264+
texture.fromRawImage(image);
266265
return texture.build();
267266
}
268267

@@ -292,7 +291,7 @@ Future<Uint8List?> processPNG(
292291
texture.setTextureScale(hByteScale, vPixelScale);
293292
}
294293

295-
texture.fromPNGImage(image);
294+
texture.fromRawImage(image);
296295

297296
if (pair.item2.textureType == TextureType.Palette8bpp || pair.item2.textureType == TextureType.Palette4bpp) {
298297
if (texture.isPalette) {

lib/features/create/create_replace_textures/components/otr_content_view.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ Widget OTRContent(CreateReplaceTexturesViewModel viewModel, BuildContext context
1717
enabled: false,
1818
decoration: InputDecoration(
1919
border: const OutlineInputBorder(),
20-
labelText: viewModel.selectedOTRPath ?? i18n.otrContentView_otrPath,
20+
labelText: viewModel.selectedOTRPaths.isEmpty ? i18n.otrContentView_otrPath :
21+
viewModel.selectedOTRPaths.length > 1 ?
22+
viewModel.selectedOTRPaths.map((e) => e.split('/').last).join(', ')
23+
: viewModel.selectedOTRPaths.first.split('/').last,
2124
),
2225
)),
2326
const SizedBox(width: 12),
@@ -56,7 +59,7 @@ Widget OTRContent(CreateReplaceTexturesViewModel viewModel, BuildContext context
5659
Padding(
5760
padding: const EdgeInsets.only(top: 20.0),
5861
child: ElevatedButton(
59-
onPressed: viewModel.selectedOTRPath?.isEmpty == false && !viewModel.isProcessing && viewModel.processedFiles.isEmpty
62+
onPressed: viewModel.selectedOTRPaths.isEmpty == false && !viewModel.isProcessing && viewModel.processedFiles.isEmpty
6063
? viewModel.onProcessOTR : null,
6164
style: ElevatedButton.styleFrom(minimumSize: Size(
6265
MediaQuery.of(context).size.width * 0.5, 50)

lib/features/create/create_replace_textures/create_replace_textures_viewmodel.dart

Lines changed: 96 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:file_picker/file_picker.dart';
77
import 'package:flutter/services.dart';
88
import 'package:flutter/foundation.dart';
99
import 'package:flutter_storm/flutter_storm.dart';
10-
import 'package:flutter_storm/flutter_storm_bindings.dart';
1110
import 'package:flutter_storm/flutter_storm_defines.dart';
1211
import 'package:image/image.dart';
1312
import 'package:retro/models/texture_manifest_entry.dart';
@@ -27,16 +26,18 @@ class CreateReplaceTexturesViewModel extends ChangeNotifier {
2726
CreateReplacementTexturesStep currentStep =
2827
CreateReplacementTexturesStep.question;
2928
String? selectedFolderPath;
30-
String? selectedOTRPath;
29+
List<String> selectedOTRPaths = [];
3130
bool isProcessing = false;
3231
HashMap<String, dynamic> processedFiles = HashMap();
32+
3333
String fontData = "assets/FontData";
3434
String fontTLUT = "assets/FontTLUT[%d].png";
35+
String fontTextureName = "textures/font/sGfxPrintFontData";
3536

3637
reset() {
3738
currentStep = CreateReplacementTexturesStep.question;
3839
selectedFolderPath = null;
39-
selectedOTRPath = null;
40+
selectedOTRPaths = [];
4041
isProcessing = false;
4142
processedFiles = HashMap();
4243
notifyListeners();
@@ -58,19 +59,22 @@ class CreateReplaceTexturesViewModel extends ChangeNotifier {
5859
HashMap<String, ProcessedFilesInFolder>? processedFiles = await compute(processFolder, selectedFolderPath!);
5960
if (processedFiles == null) {
6061
// TODO: Handle this error.
61-
} else {
62-
this.processedFiles = processedFiles;
63-
}
62+
log("Error processing folder: $selectedFolderPath");
63+
} else {
64+
this.processedFiles = processedFiles;
65+
}
66+
6467
isProcessing = false;
6568
notifyListeners();
6669
}
6770
}
6871

6972
onSelectOTR() async {
70-
FilePickerResult? result = await FilePicker.platform.pickFiles(allowMultiple: false, type: FileType.custom, allowedExtensions: ['otr']);
73+
FilePickerResult? result = await FilePicker.platform.pickFiles(allowMultiple: true, type: FileType.custom, allowedExtensions: ['otr']);
7174
if (result != null && result.files.isNotEmpty) {
72-
selectedOTRPath = result.paths.first;
73-
if (selectedOTRPath == null) {
75+
// save paths filtering out nulls
76+
selectedOTRPaths = result.paths.whereType<String>().toList();
77+
if (selectedOTRPaths.isEmpty) {
7478
// TODO: Handle this error
7579
return;
7680
}
@@ -80,7 +84,7 @@ class CreateReplaceTexturesViewModel extends ChangeNotifier {
8084
}
8185

8286
onProcessOTR() async {
83-
if (selectedOTRPath == null) {
87+
if (selectedOTRPaths.isEmpty) {
8488
// TODO: Handle this error
8589
return;
8690
}
@@ -94,38 +98,52 @@ class CreateReplaceTexturesViewModel extends ChangeNotifier {
9498
isProcessing = true;
9599
notifyListeners();
96100

97-
HashMap<String, TextureManifestEntry>? processedFiles = await compute(processOTR, Tuple2(selectedOTRPath!, selectedDirectory));
98-
if (processedFiles == null) {
101+
// Process OTR
102+
HashMap<String, TextureManifestEntry>? otrFiles = await compute(processOTR, Tuple2(selectedOTRPaths, selectedDirectory));
103+
if (otrFiles == null) {
99104
// TODO: Handle this error.
100105
} else {
101-
this.processedFiles = processedFiles;
106+
processedFiles = otrFiles;
102107
}
103108

104-
String otrName = selectedOTRPath!.split(Platform.pathSeparator).last.split(".").first;
105-
await dumpFont(path.join(selectedDirectory, otrName), (TextureManifestEntry entry) {
106-
this.processedFiles["textures/fonts"] = entry;
109+
// Dump font
110+
await dumpFont(selectedDirectory, (TextureManifestEntry entry) {
111+
processedFiles[fontTextureName] = entry;
107112
});
108113

114+
// Write out the processed files to disk
115+
String? manifestOutputPath = "$selectedDirectory/manifest.json";
116+
File manifestFile = File(manifestOutputPath);
117+
await manifestFile.create(recursive: true);
118+
String dataToWrite = jsonEncode(processedFiles);
119+
await manifestFile.writeAsString(dataToWrite);
120+
109121
isProcessing = false;
110-
notifyListeners();
122+
notifyListeners();
111123
}
112124

113125
dumpFont(String outputPath, Function onProcessed) async {
126+
Image fontImage = Image(width: 16 * 4, height: 256, numChannels: 4, withPalette: false);
127+
114128
Texture tex = Texture.empty();
115-
tex.open((await rootBundle.load(fontData)).buffer.asUint8List());
129+
final ByteData data = await rootBundle.load(fontData);
130+
tex.open(data.buffer.asUint8List());
116131
for(int id = 0; id < 4; id++){
117132
Texture tlut = Texture.empty();
118133
tex.tlut = tlut;
119134
tlut.textureType = TextureType.RGBA32bpp;
120-
tlut.fromPNGImage(decodePng((await rootBundle.load(fontTLUT.replaceAll('%d', id.toString()))).buffer.asUint8List())!);
121-
122-
Uint8List pngBytes = tex.toPNGBytes();
123-
File textureFile = File(path.join(outputPath, "textures/font/sGfxPrintFontData[$id].png"));
124-
textureFile.createSync(recursive: true);
125-
textureFile.writeAsBytesSync(pngBytes);
126-
String hash = sha256.convert(textureFile.readAsBytesSync()).toString();
127-
onProcessed(TextureManifestEntry(hash, tex.textureType, tex.width, tex.height));
135+
final ByteData data = await rootBundle.load(fontTLUT.replaceAll('%d', id.toString()));
136+
final Image pngImage = decodePng(data.buffer.asUint8List())!;
137+
tlut.fromRawImage(pngImage);
138+
compositeImage(fontImage, decodePng(tex.toPNGBytes())!, dstX: id * 16, dstY: 0);
128139
}
140+
141+
File textureFile = File(path.join(outputPath, "$fontTextureName.png"));
142+
Uint8List pngBytes = encodePng(fontImage);
143+
await textureFile.create(recursive: true);
144+
await textureFile.writeAsBytes(pngBytes);
145+
String hash = sha256.convert(pngBytes).toString();
146+
onProcessed(TextureManifestEntry(hash, tex.textureType, fontImage.width, fontImage.height));
129147
}
130148
}
131149

@@ -142,11 +160,13 @@ Future<HashMap<String, ProcessedFilesInFolder>?> processFolder(String folderPath
142160
String manifestContents = await manifestFile.readAsString();
143161
Map<String, dynamic> manifest = jsonDecode(manifestContents);
144162

145-
// find all pngs in folder
163+
// find all images in folder
164+
List<String> supportedExtensions = ['.png', '.jpeg', '.jpg'];
146165
List<FileSystemEntity> files = Directory(folderPath).listSync(recursive: true);
166+
List<FileSystemEntity> texFiles = files.where((file) => supportedExtensions.contains(path.extension(file.path))).toList();
147167

148-
// for each png, check if it's in the manifest
149-
for (FileSystemEntity rawFile in files) {
168+
// for each tex image, check if it's in the manifest
169+
for (FileSystemEntity rawFile in texFiles) {
150170
File texFile = File(p.normalize(rawFile.path));
151171
String texPathRelativeToFolder = p.normalize(texFile.path.split("$folderPath/").last.split('.').first);
152172
if (manifest.containsKey(texPathRelativeToFolder)) {
@@ -173,66 +193,62 @@ Future<HashMap<String, ProcessedFilesInFolder>?> processFolder(String folderPath
173193
return processedFiles;
174194
}
175195

176-
Future<HashMap<String, TextureManifestEntry>?> processOTR(Tuple2<String, String> params) async {
196+
Future<HashMap<String, TextureManifestEntry>?> processOTR(Tuple2<List<String>, String> params) async {
177197
try {
178198
bool fileFound = false;
179199
HashMap<String, TextureManifestEntry> processedFiles = HashMap();
180200

181-
log("Processing OTR: ${params.item1}");
182-
MPQArchive? mpqArchive = MPQArchive.open(params.item1, 0, MPQ_OPEN_READ_ONLY);
183-
184-
FileFindResource hFind = FileFindResource();
185-
mpqArchive.findFirstFile("*", hFind, null);
186-
187201
// if folder we'll export to exists, delete it
188-
String otrName = params.item1.split(Platform.pathSeparator).last.split(".").first;
189-
Directory dir = Directory("${params.item2}/$otrName");
202+
Directory dir = Directory(params.item2);
190203
if (dir.existsSync()) {
191-
log("Deleting existing folder: ${params.item2}/$otrName");
204+
log("Deleting existing folder: ${params.item2}");
192205
await dir.delete(recursive: true);
193206
}
194-
195-
// process first file
196-
String? fileName = hFind.fileName();
197-
await processFile(fileName!, mpqArchive, "${params.item2}/$otrName/$fileName.png", (TextureManifestEntry entry) {
198-
processedFiles[fileName] = entry;
199-
});
200-
201-
do {
202-
try {
203-
mpqArchive.findNextFile(hFind);
204-
fileFound = true;
205-
206-
String? fileName = hFind.fileName();
207-
if (fileName == null || fileName == "(signature)" || fileName == "(listfile)" || fileName == "(attributes)") {
208-
continue;
209-
}
210-
211-
log("Processing file: $fileName");
212-
bool processed = await processFile(fileName, mpqArchive, "${params.item2}/$otrName/$fileName.png", (TextureManifestEntry entry) {
213-
processedFiles[fileName] = entry;
214-
});
215-
216-
if (!processed) {
217-
continue;
207+
208+
for (String otrPath in params.item1) {
209+
log("Processing OTR: $otrPath");
210+
MPQArchive? mpqArchive = MPQArchive.open(otrPath, 0, MPQ_OPEN_READ_ONLY);
211+
212+
FileFindResource hFind = FileFindResource();
213+
mpqArchive.findFirstFile("*", hFind, null);
214+
215+
// process first file
216+
String? fileName = hFind.fileName();
217+
await processFile(fileName!, mpqArchive, "${params.item2}/$fileName", (TextureManifestEntry entry) {
218+
processedFiles[fileName] = entry;
219+
});
220+
221+
do {
222+
try {
223+
mpqArchive.findNextFile(hFind);
224+
fileFound = true;
225+
226+
String? fileName = hFind.fileName();
227+
if (fileName == null || fileName == SIGNATURE_NAME || fileName == LISTFILE_NAME || fileName == ATTRIBUTES_NAME) {
228+
continue;
229+
}
230+
231+
log("Processing file: $fileName");
232+
bool processed = await processFile(fileName, mpqArchive, "${params.item2}/$fileName", (TextureManifestEntry entry) {
233+
processedFiles[fileName] = entry;
234+
});
235+
236+
if (!processed) {
237+
continue;
238+
}
239+
} on StormLibException catch (e) {
240+
log("Got a StormLib error: ${e.message}");
241+
fileFound = false;
242+
} on Exception catch (e) {
243+
log("Got an error: $e");
244+
fileFound = false;
218245
}
219-
} on StormLibException catch (e) {
220-
log("Got a StormLib error: ${e.message}");
221-
fileFound = false;
222-
} on Exception catch (e) {
223-
log("Got an error: $e");
224-
fileFound = false;
225-
}
226-
} while (fileFound);
246+
} while (fileFound);
227247

228-
// Write out the processed files to disk
229-
String? manifestOutputPath = "${params.item2}/$otrName/manifest.json";
230-
File manifestFile = File(manifestOutputPath);
231-
await manifestFile.create(recursive: true);
232-
String dataToWrite = jsonEncode(processedFiles);
233-
await manifestFile.writeAsString(dataToWrite);
234-
235-
hFind.close();
248+
hFind.close();
249+
mpqArchive.close();
250+
}
251+
236252
return processedFiles;
237253
} on StormLibException catch (e) {
238254
log("Failed to find next file: ${e.message}");
@@ -262,7 +278,7 @@ Future<bool> processFile(String fileName, MPQArchive mpqArchive, String outputPa
262278
texture.open(fileData);
263279

264280
Uint8List pngBytes = texture.toPNGBytes();
265-
File textureFile = File(outputPath);
281+
File textureFile = File("$outputPath.png");
266282
await textureFile.create(recursive: true);
267283
await textureFile.writeAsBytes(pngBytes);
268284
Uint8List textureBytes = await textureFile.readAsBytes();

lib/features/debug/debug_convert_textures/debug_convert_textures_screen.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import 'package:retro/otr/types/texture.dart';
1010
import 'package:retro/ui/components/custom_scaffold.dart';
1111
import 'package:path/path.dart' as path;
1212

13-
class DebugConvertTexturesScreen extends StatefulWidget {
14-
const DebugConvertTexturesScreen({super.key});
13+
class DebugGeneratorFontsScreen extends StatefulWidget {
14+
const DebugGeneratorFontsScreen({super.key});
1515

1616
@override
17-
State<DebugConvertTexturesScreen> createState() => _DebugConvertTexturesScreenState();
17+
State<DebugGeneratorFontsScreen> createState() => _DebugGeneratorFontsScreenState();
1818
}
1919

20-
class _DebugConvertTexturesScreenState extends State<DebugConvertTexturesScreen> {
20+
class _DebugGeneratorFontsScreenState extends State<DebugGeneratorFontsScreen> {
2121

2222
TextureType selectedTextureType = TextureType.RGBA32bpp;
2323
Texture? textureData;
@@ -107,7 +107,7 @@ class _DebugConvertTexturesScreenState extends State<DebugConvertTexturesScreen>
107107
textureData = Texture.empty();
108108
textureData?.textureType = selectedTextureType;
109109
Image image = decodePng(textureFile!.readAsBytesSync())!;
110-
textureData!.fromPNGImage(image);
110+
textureData!.fromRawImage(image);
111111
if(selectedTextureType.name.contains("Palette")){
112112
textureData!.isPalette = true;
113113
}
@@ -130,7 +130,7 @@ class _DebugConvertTexturesScreenState extends State<DebugConvertTexturesScreen>
130130
Texture tlut = Texture.empty();
131131
tlut.textureType = TextureType.RGBA32bpp;
132132
Image image = decodePng(File(result.files.single.path!).readAsBytesSync())!;
133-
tlut.fromPNGImage(image);
133+
tlut.fromRawImage(image);
134134
textureData!.tlut = tlut;
135135
textureBytes = textureData!.toPNGBytes();
136136
});

0 commit comments

Comments
 (0)