diff --git a/lib/fileTypeUtils/smd/smdReader.dart b/lib/fileTypeUtils/smd/smdReader.dart new file mode 100644 index 00000000..2ae79fcd --- /dev/null +++ b/lib/fileTypeUtils/smd/smdReader.dart @@ -0,0 +1,30 @@ + +import 'dart:io'; + +import '../utils/ByteDataWrapper.dart'; + +class SmdEntry { + final String id; + final int indexX10; + final String text; + + const SmdEntry(this.id, this.indexX10, this.text); +} + +Future> readSmdFile(String path) async { + var file = File(path); + var bytes = await file.readAsBytes(); + var reader = ByteDataWrapper(bytes.buffer); + var entries = []; + int count = reader.readUint32(); + for (int i = 0; i < count; i++) { + String id = reader.readString(0x80, encoding: StringEncoding.utf16); + int indexX10 = reader.readUint64(); + String text = reader.readString(0x800, encoding: StringEncoding.utf16); + var zerosRemover = RegExp("\x00+\$"); + id = id.replaceAll(zerosRemover, ""); + text = text.replaceAll(zerosRemover, ""); + entries.add(SmdEntry(id, indexX10, text)); + } + return entries; +} diff --git a/lib/fileTypeUtils/smd/smdWriter.dart b/lib/fileTypeUtils/smd/smdWriter.dart new file mode 100644 index 00000000..90c4168a --- /dev/null +++ b/lib/fileTypeUtils/smd/smdWriter.dart @@ -0,0 +1,22 @@ + +import 'dart:io'; +import 'dart:typed_data'; + +import '../utils/ByteDataWrapper.dart'; +import 'smdReader.dart'; + +Future saveSmd(List entries, String path) async { + var totalSize = 4 + entries.length * 0x888; + var bytes = ByteDataWrapper(ByteData(totalSize).buffer); + bytes.writeUint32(entries.length); + for (var entry in entries) { + var id = entry.id.padRight(0x40, '\x00'); + var text = entry.text.padRight(0x400, '\x00'); + bytes.writeString(id, StringEncoding.utf16); + bytes.writeUint64(entry.indexX10); + bytes.writeString(text, StringEncoding.utf16); + } + + var file = File(path); + await file.writeAsBytes(bytes.buffer.asUint8List()); +} diff --git a/lib/stateManagement/FileHierarchy.dart b/lib/stateManagement/FileHierarchy.dart index 35d20afd..72768945 100644 --- a/lib/stateManagement/FileHierarchy.dart +++ b/lib/stateManagement/FileHierarchy.dart @@ -419,6 +419,11 @@ class TmdHierarchyEntry extends FileHierarchyEntry { : super(name, path, false, true); } +class SmdHierarchyEntry extends FileHierarchyEntry { + SmdHierarchyEntry(StringProp name, String path) + : super(name, path, false, true); +} + class OpenHierarchyManager extends NestedNotifier with Undoable { HierarchyEntry? _selectedEntry; @@ -451,7 +456,7 @@ class OpenHierarchyManager extends NestedNotifier with Undoable } else if (filePath.endsWith(".xml")) { if (await File(filePath).exists()) - entry = openXmlScript(filePath, parent: parent); + entry = openGenericFile(filePath, parent, (n, p) => XmlScriptHierarchyEntry(n, p)); else throw FileSystemException("File not found: $filePath"); } @@ -463,7 +468,7 @@ class OpenHierarchyManager extends NestedNotifier with Undoable } else if (filePath.endsWith(".rb")) { if (await File(filePath).exists()) - entry = openRbScript(filePath, parent: parent); + entry = openGenericFile(filePath, parent, ((n, p) => RubyScriptHierarchyEntry(n, p))); else throw FileSystemException("File not found: $filePath"); } @@ -475,7 +480,13 @@ class OpenHierarchyManager extends NestedNotifier with Undoable } else if (filePath.endsWith(".tmd")) { if (await File(filePath).exists()) - entry = openTmdFile(filePath, parent: parent); + entry = openGenericFile(filePath, parent, (n, p) => TmdHierarchyEntry(n, p),); + else + throw FileSystemException("File not found: $filePath"); + } + else if (filePath.endsWith(".smd")) { + if (await File(filePath).exists()) + entry = openGenericFile(filePath, parent, (n ,p) => SmdHierarchyEntry(n, p)); else throw FileSystemException("File not found: $filePath"); } @@ -527,7 +538,7 @@ class OpenHierarchyManager extends NestedNotifier with Undoable // process DAT files List> futures = []; datFilePaths ??= await getDatFileList(datExtractDir); - const supportedFileEndings = { ".pak", "_scp.bin", ".tmd" }; + const supportedFileEndings = { ".pak", "_scp.bin", ".tmd", ".smd" }; for (var file in datFilePaths) { if (supportedFileEndings.every((ending) => !file.endsWith(ending))) continue; @@ -541,7 +552,9 @@ class OpenHierarchyManager extends NestedNotifier with Undoable else if (file.endsWith("_scp.bin")) futures.add(openBinMrbScript(file, parent: datEntry)); else if (file.endsWith(".tmd")) - openTmdFile(file, parent: datEntry); + openGenericFile(file, parent, (n, p) => TmdHierarchyEntry(n, p)); + else if (file.endsWith(".smd")) + openGenericFile(file, parent, (n ,p) => SmdHierarchyEntry(n, p)); else throw FileSystemException("Unsupported file type: $file"); } @@ -628,28 +641,14 @@ class OpenHierarchyManager extends NestedNotifier with Undoable if (!await File(xmlFilePath).exists()) { await yaxFileToXmlFile(yaxFilePath); } - return openXmlScript(xmlFilePath, parent: parent); + return openGenericFile(xmlFilePath,parent, (n, p) => XmlScriptHierarchyEntry(n, p)); } - HierarchyEntry openXmlScript(String xmlFilePath, { HierarchyEntry? parent }) { - var existing = findRecWhere((entry) => entry is XmlScriptHierarchyEntry && entry.path == xmlFilePath); + HierarchyEntry openGenericFile(String filePath, HierarchyEntry? parent, HierarchyEntry Function(StringProp n, String p) make) { + var existing = findRecWhere((entry) => entry is T && entry.path == filePath); if (existing != null) return existing; - var entry = XmlScriptHierarchyEntry(StringProp(path.basename(xmlFilePath)), xmlFilePath); - if (parent != null) - parent.add(entry); - else - add(entry); - - return entry; - } - - HierarchyEntry openRbScript(String rbFilePath, { HierarchyEntry? parent }) { - var existing = findRecWhere((entry) => entry is RubyScriptHierarchyEntry && entry.path == rbFilePath); - if (existing != null) - return existing; - - var entry = RubyScriptHierarchyEntry(StringProp(path.basename(rbFilePath)), rbFilePath); + var entry = make(StringProp(path.basename(filePath)), filePath); if (parent != null) parent.add(entry); else @@ -669,21 +668,7 @@ class OpenHierarchyManager extends NestedNotifier with Undoable await binFileToRuby(binFilePath); } - return openRbScript(rbPath, parent: parent); - } - - HierarchyEntry openTmdFile(String tmdFilePath, { HierarchyEntry? parent }) { - var existing = findRecWhere((entry) => entry is TmdHierarchyEntry && entry.path == tmdFilePath); - if (existing != null) - return existing; - - var entry = TmdHierarchyEntry(StringProp(path.basename(tmdFilePath)), tmdFilePath); - if (parent != null) - parent.add(entry); - else - add(entry); - - return entry; + return openGenericFile(rbPath, parent, (n, p) => RubyScriptHierarchyEntry(n, p)); } @override diff --git a/lib/stateManagement/openFileTypes.dart b/lib/stateManagement/openFileTypes.dart index f9c70651..67fad195 100644 --- a/lib/stateManagement/openFileTypes.dart +++ b/lib/stateManagement/openFileTypes.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:path/path.dart'; import 'package:xml/xml.dart'; +import '../fileTypeUtils/smd/smdReader.dart'; +import '../fileTypeUtils/smd/smdWriter.dart'; import '../fileTypeUtils/tmd/tmdReader.dart'; import '../fileTypeUtils/tmd/tmdWriter.dart'; import '../utils.dart'; @@ -14,6 +16,7 @@ import 'Property.dart'; import 'changesExporter.dart'; import 'hasUuid.dart'; import 'miscValues.dart'; +import 'otherFileTypes/SmdFileData.dart'; import 'otherFileTypes/TmdFileData.dart'; import 'undoable.dart'; import 'xmlProps/xmlProp.dart'; @@ -47,6 +50,8 @@ class OpenFileData extends ChangeNotifier with Undoable, HasUuid { return RubyFileData(name, path, secondaryName: secondaryName); else if (path.endsWith(".tmd")) return TmdFileData(name, path, secondaryName: secondaryName); + else if (path.endsWith(".smd")) + return SmdFileData(name, path, secondaryName: secondaryName); else return TextFileData(name, path, secondaryName: secondaryName); } @@ -58,6 +63,8 @@ class OpenFileData extends ChangeNotifier with Undoable, HasUuid { return FileType.preferences; else if (path.endsWith(".tmd")) return FileType.tmd; + else if (path.endsWith(".smd")) + return FileType.smd; else return FileType.text; } @@ -315,3 +322,40 @@ class TmdFileData extends OpenFileData { super.dispose(); } } + +class SmdFileData extends OpenFileData { + SmdData? smdData; + + SmdFileData(super.name, super.path, { super.secondaryName }); + + @override + Future load() async { + if (_loadingState != LoadingState.notLoaded) + return; + _loadingState = LoadingState.loading; + + var smdEntries = await readSmdFile(path); + smdData = SmdData.from(smdEntries, basenameWithoutExtension(path)); + smdData!.fileChangeNotifier.addListener(() { + hasUnsavedChanges = true; + }); + + await super.load(); + } + + @override + Future save() async { + await saveSmd(smdData!.toEntries(), path); + + var datDir = dirname(path); + changedDatFiles.add(datDir); + + await super.save(); + } + + @override + void dispose() { + smdData?.dispose(); + super.dispose(); + } +} diff --git a/lib/stateManagement/otherFileTypes/SmdFileData.dart b/lib/stateManagement/otherFileTypes/SmdFileData.dart new file mode 100644 index 00000000..05295959 --- /dev/null +++ b/lib/stateManagement/otherFileTypes/SmdFileData.dart @@ -0,0 +1,98 @@ +// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + +import 'package:flutter/material.dart'; + +import '../../fileTypeUtils/smd/smdReader.dart'; +import '../../widgets/propEditors/customXmlProps/tableEditor.dart'; +import '../Property.dart'; +import '../hasUuid.dart'; +import '../nestedNotifier.dart'; +import '../undoable.dart'; + +class SmdEntryData with HasUuid { + final StringProp id; + final StringProp text; + final ChangeNotifier _anyChangeNotifier; + + SmdEntryData({ required this.id, required this.text, required ChangeNotifier anyChangeNotifier }) + : _anyChangeNotifier = anyChangeNotifier { + id.addListener(_anyChangeNotifier.notifyListeners); + text.addListener(_anyChangeNotifier.notifyListeners); + } +} + +class SmdData extends NestedNotifier with CustomTableConfig, Undoable { + final ChangeNotifier fileChangeNotifier; + + SmdData(List entries, String fileName, this.fileChangeNotifier) + : super(entries) { + name = fileName; + columnNames = ["ID", "Text"]; + rowCount = NumberProp(entries.length, true); + } + + SmdData.from(List rawEntries, String fileName) + : fileChangeNotifier = ChangeNotifier(), + super([]) { + addAll(rawEntries.map((e) { + var idProp = StringProp(e.id); + var textProp = StringProp(e.text); + textProp.transform = (str) => str; + return SmdEntryData( + id: idProp, + text: textProp, + anyChangeNotifier: fileChangeNotifier, + ); + })); + name = fileName; + columnNames = ["ID", "Text"]; + rowCount = NumberProp(rawEntries.length, true); + } + + List toEntries() + => List.generate(length, (i) => SmdEntry(this[i].id.value, i * 10, this[i].text.value)); + + @override + void onRowAdd() { + var idProp = StringProp("ID"); + var textProp = StringProp("Text"); + textProp.transform = (str) => str; + add(SmdEntryData( + id: idProp, + text: textProp, + anyChangeNotifier: fileChangeNotifier, + )); + rowCount.value++; + fileChangeNotifier.notifyListeners(); + } + + @override + void onRowRemove(int index) { + removeAt(index); + rowCount.value--; + fileChangeNotifier.notifyListeners(); + } + + @override + RowConfig rowPropsGenerator(int index) { + var entry = this[index]; + return RowConfig( + key: Key(entry.uuid), + cells: [ + CellConfig(prop: entry.id), + CellConfig(prop: entry.text, allowMultiline: true), + ], + ); + } + + @override + void restoreWith(Undoable snapshot) { + // TODO: implement restoreWith + } + + @override + Undoable takeSnapshot() { + // TODO: implement takeSnapshot + throw UnimplementedError(); + } +} diff --git a/lib/widgets/FileHierarchyExplorer/HierarchyEntryWidget.dart b/lib/widgets/FileHierarchyExplorer/HierarchyEntryWidget.dart index d137adff..8f4c04f0 100644 --- a/lib/widgets/FileHierarchyExplorer/HierarchyEntryWidget.dart +++ b/lib/widgets/FileHierarchyExplorer/HierarchyEntryWidget.dart @@ -40,7 +40,7 @@ class _HierarchyEntryState extends ChangeNotifierState { return Icon(Icons.workspaces, color: iconColor, size: 15); else if (widget.entry is XmlScriptHierarchyEntry || widget.entry is RubyScriptHierarchyEntry) return Icon(Icons.description, color: iconColor, size: 15); - else if (widget.entry is TmdHierarchyEntry) + else if (widget.entry is TmdHierarchyEntry || widget.entry is SmdHierarchyEntry) return Icon(Icons.subtitles, color: iconColor, size: 15); return null; diff --git a/lib/widgets/filesView/FileType.dart b/lib/widgets/filesView/FileType.dart index 85175964..91ff399c 100644 --- a/lib/widgets/filesView/FileType.dart +++ b/lib/widgets/filesView/FileType.dart @@ -12,6 +12,7 @@ enum FileType { xml, preferences, tmd, + smd, } Widget makeFileEditor(OpenFileData content) { @@ -22,6 +23,8 @@ Widget makeFileEditor(OpenFileData content) { return PreferencesEditor(prefs: content as PreferencesData); case FileType.tmd: return TableFileEditor(file: content, getTableConfig: () => (content as TmdFileData).tmdData!); + case FileType.smd: + return TableFileEditor(file: content, getTableConfig: () => (content as SmdFileData).smdData!); default: return TextFileEditor(fileContent: content as TextFileData); } diff --git a/lib/widgets/theme/customTheme.dart b/lib/widgets/theme/customTheme.dart index 76152934..98f9e35f 100644 --- a/lib/widgets/theme/customTheme.dart +++ b/lib/widgets/theme/customTheme.dart @@ -212,7 +212,7 @@ class NierThemeExtension extends ThemeExtension { } Color colorOfFiletype(HierarchyEntry entry) { - if (entry is XmlScriptHierarchyEntry || entry is RubyScriptHierarchyEntry || entry is TmdHierarchyEntry) + if (entry is XmlScriptHierarchyEntry || entry is RubyScriptHierarchyEntry || entry is TmdHierarchyEntry || entry is TmdHierarchyEntry) return filetypeXmlColor!; if (entry is PakHierarchyEntry) return filetypePakColor!;