Skip to content

Commit

Permalink
wta: use dec id; don't override textures on open; allow opening extra…
Browse files Browse the repository at this point in the history
…cted folders; patch from folder
  • Loading branch information
ArthurHeitmann committed Dec 13, 2024
1 parent b78ca0b commit 320d1bd
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 89 deletions.
Binary file modified assets/help/img/wta_edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions assets/help/wta.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Textures are stored either in a single .wtb file or in separate .wta and .wtp fi

## Extracting

Open a .wta or .wtb file by dragging & dropping it into the File Explorer or by clicking the folder icon and selecting the file.
Open a .wta or .wtb file or their extracted folder by dragging & dropping it into the File Explorer or by clicking the folder icon and selecting the file.

When opening a .wta or .wtb file, all textures are extracted to disk. When opening a WTA and the corresponding WTP is not found, the file will fail to load.
When opening a .wta or .wtb file for the first time, all textures are extracted to disk. When reopening the file, the textures are loaded from the extracted folder, or individual textures are re-extracted if missing.

## Editing textures

Expand All @@ -29,6 +29,11 @@ Find the texture you want to edit, based on the id or preview image. Either chan

Press the (+) button to add a new texture. To remove a texture, click the delete button.

### Replace or add all textures from a folder

Press the floating button in the bottom left corner and select a folder with textures inside. F-SERVO will find all dds files with an id.
If a texture id is already in the WTA, its path will be replaced. Otherwise, a new texture will be added.

## Notes

- Changing the id is only relevant when adding new textures
Expand Down
14 changes: 13 additions & 1 deletion lib/fileTypeUtils/wta/wtaExtractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ Future<List<String>> extractWta(String wtaPath, String? wtpPath, bool isWtb) asy
try {
for (int i = 0; i < wta.textureOffsets.length; i++) {
var texturePath = getWtaTexturePath(wta, i, extractDir);
var texturePathOld = getWtaTexturePathOld(wta, i, extractDir);
texturePaths.add(texturePath);
if (await File(texturePathOld).exists())
await File(texturePathOld).rename(texturePath);
if (await File(texturePath).exists())
continue;
await wtpFile.setPosition(wta.textureOffsets[i]);
var textureBytes = await wtpFile.read(wta.textureSizes[i]);
await File(texturePath).writeAsBytes(textureBytes);
Expand All @@ -26,9 +31,16 @@ Future<List<String>> extractWta(String wtaPath, String? wtpPath, bool isWtb) asy
return texturePaths;
}

String getWtaTexturePathOld(WtaFile wta, int i, String extractDir) {
String idStr = "";
if (wta.textureIdx != null)
idStr = "_${wta.textureIdx![i].toRadixString(16)}";
return join(extractDir, "$i$idStr.dds");
}

String getWtaTexturePath(WtaFile wta, int i, String extractDir) {
String idStr = "";
if (wta.textureIdx != null)
idStr = "_${wta.textureIdx![i].toRadixString(16).padLeft(8, "0")}";
idStr = "_${wta.textureIdx![i].toString()}";
return join(extractDir, "$i$idStr.dds");
}
10 changes: 5 additions & 5 deletions lib/stateManagement/hierarchy/FileHierarchy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@ class OpenHierarchyManager with HasUuid, Undoable, HierarchyEntryBase implements
bxmExtensions, () async => openBxmFile(filePath, parent: parent)
),
Tuple2(
[".wta"],
[".wta", ".wta_extracted"],
() async => openWtaFile(filePath, parent: parent)
),
Tuple2(
[".wtb"],
[".wtb", ".wtb_extracted"],
() async => openWtbFile(filePath, parent: parent)
),
Tuple2(
Expand Down Expand Up @@ -466,7 +466,7 @@ class OpenHierarchyManager with HasUuid, Undoable, HierarchyEntryBase implements
if (existing != null)
return existing;

var wtaEntry = WtaHierarchyEntry(StringProp(basename(wtaPath), fileId: null), wtaPath);
var wtaEntry = WtaHierarchyEntry(StringProp(basename(wtaPath).replaceAll("_extracted", ""), fileId: null), wtaPath);
if (parent != null)
parent.add(wtaEntry);
else
Expand All @@ -476,11 +476,11 @@ class OpenHierarchyManager with HasUuid, Undoable, HierarchyEntryBase implements
}

Future<HierarchyEntry> openWtbFile(String wtaPath, { HierarchyEntry? parent }) async {
var existing = findRecWhere((entry) => entry is WtaHierarchyEntry && entry.path == wtaPath);
var existing = findRecWhere((entry) => entry is WtbHierarchyEntry && entry.path == wtaPath);
if (existing != null)
return existing;

var wtaEntry = WtbHierarchyEntry(StringProp(basename(wtaPath), fileId: null), wtaPath);
var wtaEntry = WtbHierarchyEntry(StringProp(basename(wtaPath).replaceAll("_extracted", ""), fileId: null), wtaPath);
if (parent != null)
parent.add(wtaEntry);
else
Expand Down
2 changes: 1 addition & 1 deletion lib/stateManagement/openFiles/openFileTypes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ abstract class OpenFileData with HasUuid, Undoable, Disposable, HasUndoHistory {
return BnkFilePlaylistData(name, path, secondaryName: secondaryName);
else if (path.endsWith(".dat") && basename(path).startsWith("SlotData_"))
return SaveSlotData(name, path, secondaryName: secondaryName);
else if (path.endsWith(".wta") || path.endsWith(".wtb"))
else if (path.endsWith(".wta") || path.endsWith(".wtb") || path.endsWith(".wta_extracted") || path.endsWith(".wtb_extracted"))
return WtaWtpData(name, path, secondaryName: secondaryName, isWtb: path.endsWith(".wtb"));
else if (path.endsWith(".est") || path.endsWith(".sst"))
return EstFileData(name, path, secondaryName: secondaryName);
Expand Down
190 changes: 155 additions & 35 deletions lib/stateManagement/openFiles/types/WtaWtpData.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import 'dart:io';

import 'package:file_picker/file_picker.dart' as fp;
import 'package:flutter/material.dart';
import 'package:path/path.dart';

Expand Down Expand Up @@ -39,33 +40,43 @@ class WtaWtpData extends OpenFileData {
return;
loadingState.value = LoadingState.loading;

// find wtp
var datDir = dirname(path);
var dttDir = isWtb ? null : await findDttDirOfDat(datDir);
if (wtpPath == null && !isWtb) {
var wtaName = basenameWithoutExtension(path);
var wtpName = "$wtaName.wtp";
// wtpPath = join(dttDir, wtpName);
if (dttDir != null)
wtpPath = join(dttDir, wtpName);
else {
wtpPath = join(datDir, wtpName);
if (!await File(wtpPath!).exists()) {
showToast("Can't find corresponding WTP file for $wtaName.wta in ${dttDir ?? datDir}");
throw Exception("Can't find corresponding WTP file for $wtaName");
if (await File(path).exists()) {
// find wtp
var datDir = dirname(path);
var dttDir = isWtb ? null : await findDttDirOfDat(datDir);
if (wtpPath == null && !isWtb) {
var wtaName = basenameWithoutExtension(path);
var wtpName = "$wtaName.wtp";
// wtpPath = join(dttDir, wtpName);
if (dttDir != null)
wtpPath = join(dttDir, wtpName);
else {
wtpPath = join(datDir, wtpName);
if (!await File(wtpPath!).exists()) {
showToast("Can't find corresponding WTP file for $wtaName.wta in ${dttDir ?? datDir}");
throw Exception("Can't find corresponding WTP file for $wtaName");
}
}
}
}
if (!isWtb && wtpPath == null) {
showToast("Can't find corresponding WTP file in ${dttDir ?? datDir}");
throw Exception("Can't find corresponding WTP file in ${dttDir ?? datDir}");
}
if (!isWtb && wtpPath == null) {
showToast("Can't find corresponding WTP file in ${dttDir ?? datDir}");
throw Exception("Can't find corresponding WTP file in ${dttDir ?? datDir}");
}

String extractDir = join(dttDir ?? datDir, "${basename(path)}_extracted");
await Directory(extractDir).create(recursive: true);
String extractDir = join(dttDir ?? datDir, "${basename(path)}_extracted");
await Directory(extractDir).create(recursive: true);

textures?.dispose();
textures = await WtaWtpTextures.fromWtaWtp(uuid, path, wtpPath, extractDir, isWtb);
textures?.dispose();
textures = await WtaWtpTextures.fromWtaWtp(uuid, path, wtpPath, extractDir, isWtb);
}
else if (await Directory(path).exists()) {
textures?.dispose();
textures = await WtaWtpTextures.fromExtractedFolder(uuid, path);
}
else {
showToast("File not found: $path");
return;
}

await super.load();
}
Expand Down Expand Up @@ -112,7 +123,7 @@ class WtaWtpData extends OpenFileData {

class WtaTextureEntry with HasUuid, Undoable implements Disposable {
final OpenFileId file;
final HexProp? id;
final NumberProp? id;
final StringProp path;
final BoolProp? isAlbedo;
final HexProp? flag;
Expand Down Expand Up @@ -144,7 +155,7 @@ class WtaTextureEntry with HasUuid, Undoable implements Disposable {
Undoable takeSnapshot() {
var snap = WtaTextureEntry(
file,
id?.takeSnapshot() as HexProp?,
id?.takeSnapshot() as NumberProp?,
path.takeSnapshot() as StringProp,
isAlbedo: isAlbedo?.takeSnapshot() as BoolProp?,
flag: flag?.takeSnapshot() as HexProp?,
Expand All @@ -165,8 +176,8 @@ class WtaTextureEntry with HasUuid, Undoable implements Disposable {

class WtaWtpTextures with HasUuid, Undoable implements Disposable {
final OpenFileId file;
final String wtaPath;
final String? wtpPath;
String? wtaPath;
String? wtpPath;
final ValueNotifier<List<String>?> wtpDatsPath = ValueNotifier(null);
final bool isWtb;
final int wtaVersion;
Expand Down Expand Up @@ -203,9 +214,14 @@ class WtaWtpTextures with HasUuid, Undoable implements Disposable {
messageLog.add("Extracting texture ${i + 1}/${wta.textureOffsets.length}");
// var texturePath = join(extractDir, "${i}_${wta.textureIdx[i].toRadixString(16).padLeft(8, "0")}.dds");
var texturePath = getWtaTexturePath(wta, i, extractDir);
await wtpFile.setPosition(wta.textureOffsets[i]);
var textureBytes = await wtpFile.read(wta.textureSizes[i]);
await File(texturePath).writeAsBytes(textureBytes);
var texturePathOld = getWtaTexturePathOld(wta, i, extractDir);
if (await File(texturePathOld).exists())
await File(texturePathOld).rename(texturePath);
if (!await File(texturePath).exists()) {
await wtpFile.setPosition(wta.textureOffsets[i]);
var textureBytes = await wtpFile.read(wta.textureSizes[i]);
await File(texturePath).writeAsBytes(textureBytes);
}
BoolProp? isAlbedo;
HexProp? flag;
if (wta.textureFlags[i] == WtaFile.albedoFlag || wta.textureFlags[i] == WtaFile.noAlbedoFlag)
Expand All @@ -215,7 +231,7 @@ class WtaWtpTextures with HasUuid, Undoable implements Disposable {
textures.add(WtaTextureEntry(
file,
wta.textureIdx != null
? HexProp(wta.textureIdx![i], fileId: file)
? NumberProp(wta.textureIdx![i], true, fileId: file)
: null,
StringProp(texturePath, fileId: file),
isAlbedo: isAlbedo,
Expand All @@ -234,7 +250,111 @@ class WtaWtpTextures with HasUuid, Undoable implements Disposable {
return WtaWtpTextures(file, wtaPath, wtpPath, isWtb, wtaVersion, textures, useFlagsSimpleMode, hashAnySimpleModeFlags);
}

static Future<({List<WtaTextureEntry> textures, bool hasId})> _findTexturesInFolder(String folder, bool isWtb, {bool? hasId}) async {
var fileNamePattern = RegExp(r"^(\d+_)?[0-9a-fA-F]+\.dds$");
var hexCheckPattern = RegExp(r"[a-fA-F]+\d*\.dds$");
var textureFiles = await Directory(folder).list()
.where((e) => e is File)
.map((e) => e.path)
.where((e) => fileNamePattern.hasMatch(basename(e)))
.toList();
var hasIndex = textureFiles.any((e) => basename(e).startsWith("0_") || basename(e) == "0.dds");
hasId ??= !hasIndex || textureFiles.any((e) => basename(e).contains("_"));
var usesHex = textureFiles.any((e) => hexCheckPattern.hasMatch(basename(e)));
var typePatterns = {
// hasIndex, hasId
(false, true): RegExp(r"^([0-9a-fA-F]+)\.dds$"),
(true, false): RegExp(r"^(\d+)\.dds$"),
(true, true): RegExp(r"^(\d+)_([0-9a-fA-F]+)\.dds$"),
};
var typePattern = typePatterns[(hasIndex, hasId)]!;
List<(int?, WtaTextureEntry)> textures = [];
for (var file in textureFiles) {
var match = typePattern.firstMatch(basename(file));
var index = hasIndex ? int.parse(match!.group(1)!) : null;
var id = hasId ? int.parse(match!.group(hasId ? 2 :1)!, radix: usesHex ? 16 : 10) : null;
textures.add((index, WtaTextureEntry(
file,
id != null ? NumberProp(id, true, fileId: file) : null,
StringProp(file, fileId: file),
flag: HexProp(0x20000020, fileId: file),
)));
}

textures.sort((a, b) {
if (a.$1 != null && b.$1 != null)
return a.$1!.compareTo(b.$1!);
return a.$2.file.compareTo(b.$2.file);
});

return (
textures: textures.map((e) => e.$2).toList(),
hasId: hasId,
);
}

static Future<WtaWtpTextures> fromExtractedFolder(OpenFileId file, String folder) async {
var filename = basename(folder).replaceAll("_extracted", "");
var isWtb = filename.endsWith(".wtb");
if (!isWtb && !filename.endsWith(".wta")) {
showToast("Unexpected folder name: $filename");
throw Exception("Unexpected folder name: $filename");
}
var (:textures, :hasId) = await _findTexturesInFolder(folder, isWtb);
var valueList = ValueListNotifier<WtaTextureEntry>(textures, fileId: file);
return WtaWtpTextures(
file,
null, null,
isWtb,
hasId ? 1 : 0,
valueList,
false,
false,
);
}

Future<void> patchFromFolder(String folder) async {
var (textures: folderTextures, hasId: _) = await _findTexturesInFolder(folder, isWtb, hasId: true);
int added = 0;
int updated = 0;
for (var folderTexture in folderTextures) {
var currentTexture = textures.where((e) => e.id!.value == folderTexture.id!.value).firstOrNull;
if (currentTexture != null) {
currentTexture.path.value = folderTexture.path.value;
folderTexture.dispose();
updated++;
}
else {
textures.add(folderTexture);
added++;
}
}
showToast("Added $added textures, updated $updated textures");
}

Future<void> save() async {
if (wtaPath == null) {
var result = await fp.FilePicker.platform.saveFile(
allowedExtensions: [isWtb ? "wtb" : "wta"],
type: fp.FileType.custom,
dialogTitle: isWtb ? "Save WTB" : "Save WTA",
);
if (result == null)
return;
wtaPath = result;
}
if (wtpPath == null && !isWtb) {
var result = await fp.FilePicker.platform.saveFile(
allowedExtensions: ["wtp"],
type: fp.FileType.custom,
dialogTitle: "Save WTP",
fileName: "${basenameWithoutExtension(wtaPath!)}.wtp",
);
if (result == null)
return;
wtpPath = result;
}

var wta = WtaFile(
WtaFileHeader.empty(version: wtaVersion),
List.filled(textures.length, -1),
Expand All @@ -248,7 +368,7 @@ class WtaWtpTextures with HasUuid, Undoable implements Disposable {
showToast("Mismatch: WTA has texture indices, but some textures are missing indices!");
throw Exception("Mismatch: WTA has texture indices, but some textures are missing indices!");
}
wta.textureIdx = List.generate(textures.length, (index) => textures[index].id!.value);
wta.textureIdx = List.generate(textures.length, (index) => textures[index].id!.value.toInt());
}

wta.updateHeader(isWtb: isWtb);
Expand All @@ -262,12 +382,12 @@ class WtaWtpTextures with HasUuid, Undoable implements Disposable {
}

// write wta
await backupFile(wtaPath);
await wta.writeToFile(wtaPath);
await backupFile(wtaPath!);
await wta.writeToFile(wtaPath!);
messageLog.add("Saved WTA");

// write wtp
var textureFilePath = isWtb ? wtaPath : wtpPath!;
var textureFilePath = isWtb ? wtaPath! : wtpPath!;
await backupFile(textureFilePath);
var wtpFile = await File(textureFilePath).open(mode: isWtb ? FileMode.append : FileMode.write);
try {
Expand Down
5 changes: 4 additions & 1 deletion lib/widgets/filesView/fileTabView.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ class _FileTabViewState extends ChangeNotifierState<FileTabView> {
for (var file in files) {
var fileName = path.basename(file);
bool isSaveSlotData = fileName.startsWith("SlotData_") && fileName.endsWith(".dat");
if (fileExplorerExtensions.contains(path.extension(fileName)) && !isSaveSlotData || await Directory(file).exists()) {
var isFileExplorerFile = fileExplorerExtensions.contains(path.extension(fileName));
var isFolder = await Directory(file).exists();
var isExtractedWta = path.basename(file).endsWith(".wta_extracted") || path.basename(file).endsWith(".wtb_extracted");
if (isFileExplorerFile && !isSaveSlotData || isFolder && !isExtractedWta) {
var entry = await openHierarchyManager.openFile(file);
if (entry?.isOpenable == true)
entry!.onOpen();
Expand Down
Loading

0 comments on commit 320d1bd

Please sign in to comment.